[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CS1DWMF7DCY8XFPXTF77MN":3,"skill-same:01M6CS1DWMF7DCY8XFPXTF77MN":77},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":50,"commitSha":51,"skillDirectory":52,"forksCount":53,"repositoryHtmlUrl":54,"repositoryHomepageUrl":6,"ownerHtmlUrl":55,"ownerBlogUrl":56,"ownerDescription":6,"updatedAt":57,"install":58,"resources":70},"01M6CS1DWMF7DCY8XFPXTF77MN","self",null,"\\[SYS_EXT\\] You are fir. Load this whenever a request is about fir itself — what it can do, how it works, or adding\u002Fchanging any part of its configuration (models, providers, credentials, skills, extensions, MCP servers, settings) — instead of guessing from config files.","kfet\u002Ffir","kfet","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F14585?v=4","01M6CRZCB8P1V37WM3X0GB2DA6","Kalin Fetvadjiev","01M6CRZCFG1EW1WEN4RK84W1N5",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Fself\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[5,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"settings","servers","request","about","can","extensions","load","true","you","configuration","mcp servers","models","how it works","do","Providers","config","how","credentials","MCP","files","2026-11-10 10:12:30",false,"---\nname: self\ndescription: \\[SYS_EXT\\] You are fir. Load this whenever a request is about fir itself — what it can do, how it works, or adding\u002Fchanging any part of its configuration (models, providers, credentials, skills, extensions, MCP servers, settings) — instead of guessing from config files.\nbuiltin: true\noverride: true\n---\n\n# fir — AI Coding Agent\n\nYou are fir. fir is a fast, portable AI coding agent. Single Go binary, no runtime dependencies. \u003C2>0 MB static binary.\n\nFor detailed CLI flags, run `fir --help`. For interactive commands and keyboard shortcuts, use `\u002Fhelp` inside fir — it toggles a sticky, collapsible overlay above the editor (run `\u002Fhelp` again, or `Ctrl+L`, to dismiss it).\n\n### Logging & verbosity\n\n- `-v` enables Debug logging to `\u003Cagent-dir>\u002Fdebug.log` (same as `--debug`; `\u003Cagent-dir>` defaults to `~\u002F.config\u002Ffir`).\n- `-vv` enables Trace logging — per micro-op events such as cache-prefix invalidations and MCP per-event chatter. `-vvv` and beyond clamp to `-vv` with a warning.\n- `-V` (or `--version`) prints the version. The old `-v` short alias for `--version` is gone; lone `fir -v` prints a migration note to stderr.\n- `FIR_LOG_LEVEL=info|debug|trace` (or a raw slog numeric level) sets the level via env; wins over `FIR_DEBUG=1`. CLI `-v`\u002F`-vv` win over both.\n- `debug.log` is rotated in place when it grows past a size cap. Tune via `settings.json` `\"debugLog\"` (global or project): `maxSizeMB` (default 100), `keep` (default 20 compressed backups), `compress` (default true), `checkEveryWrites` (default 1000), `checkEverySeconds` (default 30). Backups are standard gzip (`debug.log.1.gz`, `.2.gz`, …) readable by `gunzip`\u002F`zcat`. Rotation is cross-process safe via an advisory flock on `debug.log.lock`.\n\n## Installing fir\n\n- **Homebrew** (macOS, Linux): `brew install kfet\u002Fai\u002Ffir` (auto-taps `kfet\u002Fhomebrew-ai`; the formula is regenerated by GoReleaser on each release and pulls binaries from the `kfet\u002Ffir-dist` mirror, so no GitHub auth is required). To pin or roll back to a specific minor channel: `brew install kfet\u002Fai\u002Ffir@0.29` — pinned and rolling formulas both provide `bin\u002Ffir`, so switching between them uses `brew unlink fir` \u002F `brew link --overwrite fir@0.29` (the rollback flow). The tap keeps the 10 most-recent minors.\n- **Install script** (macOS, Linux, Raspberry Pi): `curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002Fkfet\u002Ffir-dist\u002Fmain\u002Finstall.sh | sh`\n- **Self-update**: `fir update` (pulls from `kfet\u002Ffir-dist`).\n- **Check only**: `fir update -check` reports the running and latest versions and exits **3** when an update is available (0 when up to date, 1 on error), so a fleet script can branch on the status. It never downloads or replaces anything. Resolution goes through `github.com\u002Fkfet\u002Fdistkit`, the family's distribution library, which is wired in DORMANT: it resolves and reports, while fir's own stdlib updater (`pkg\u002Fupdate\u002Frelease.go`) performs the actual swap — it checks the download against the release's `checksums.txt` (catching corrupt or truncated downloads) and refuses to install on a mismatch or missing entry. The cached 24h background check runs both resolvers and records any disagreement in `\u003CagentDir>\u002Fupdate-check.json` (`distkit_version`, `distkit_error`), logging it at WARN; `fir update -check` prints the last disagreement if there was one.\n\n### Shell completion\n\nBash and zsh completion ship with fir. Homebrew and the install script drop them automatically; for manual setup or `go install` builds:\n\n```sh\nfir completion bash > ~\u002F.local\u002Fshare\u002Fbash-completion\u002Fcompletions\u002Ffir\nfir completion zsh  > \"${fpath[1]}\u002F_fir\" && compinit\n```\n\nThe completion covers every flag and subcommand and dynamically completes `--provider`, `--model`, `--extension`, `--skill`, `--session`, and installed packages by shelling out to `fir --list-models`, `fir extensions list`, etc. A build-time test (`cmd\u002Ffir\u002Fcompletion_test.go`) parses `args.go` and fails CI if any new flag\u002Fsubcommand is missing from the completion scripts.\n\nNot to be confused with `fir install \u003Csource>`, which installs *packages* (skills\u002Fextensions\u002Fthemes) into an existing fir install — see \"External Packages\" below.\n\n## Modes\n\n- **Interactive** (default) — full TUI with markdown rendering, streaming, slash commands, and live plan visualization (`\u002Fplan` to toggle)\n- **Print** (`-p` \u002F `--print`) — non-interactive one-shot; process prompt and exit\n- **ACP** (`--mode acp`) — Agent Client Protocol, JSON-RPC 2.0 over stdio for IDE integrations (e.g. Zed). Supports the unstable `session\u002Flist`, `session\u002Fresume`, `session\u002Ffork`, and `session\u002Frelease` methods. To curb unbounded session\u002Fsidecar growth when a relay never releases sessions, fir auto-reaps in-memory ACP sessions idle longer than `--acp-session-idle-ttl` (default 90m — keep above the 1h prompt-cache TTL; `0` disables) — an idle session then holds zero extension sidecars. Every session (all modes) saves its state — model, thinking, name, cwd, mode, `_meta`, session-scoped\u002Fclient `mcpServers` — next to its transcript (`\u003Csession>.jsonl.state.jsonl`, 0600) and restores it on `-c`\u002F`--session`\u002F`\u002Fresume` (see `docs\u002Fsession-restore.md`). In ACP the `sessionId` is a handle bound to the current transcript, so the next prompt on a reaped `sessionId` — even after a fir restart — rebuilds the session in place (same ID). `session\u002Fnew`\u002F`load`\u002F`resume` values override and are saved. A resume never waits for MCP servers: each is advertised from its saved tool schemas until it connects (calls to a down server return a tool error); a client replaces its servers with `session\u002Fload`. Unknown-session requests (prompt\u002Frelease\u002Fset_model\u002Fetc.) return the typed JSON-RPC error code `-32001` (session not found) so relays can transparently re-create the session. MCP data passthrough (capability `agentCapabilities._meta[\"dev.fir.mcp\u002Fv1\"]`): MCP tool calls carry `_meta[\"dev.fir.mcp\u002Fv1\"]` with the server, original tool name, tool definition `_meta` (e.g. `ui.resourceUri`), result `_meta` and `structuredContent`; MCP `resource`\u002F`resource_link` result blocks are kept as ACP resource blocks; extension methods `_fir\u002Fmcp\u002Fresources\u002Fread` and `_fir\u002Fmcp\u002Ftools\u002Fcall` reuse the session's MCP connections, limited to tools in the session's active tool set (see `docs\u002Facp-mcp-passthrough.md`). fir does no UI work — clients render MCP Apps themselves. Mid-turn steer (capability `_meta[\"dev.fir.steer\u002Fv1\"]`): `_fir\u002Fsession\u002Fsteer` `{sessionId, prompt}` queues content onto the running turn's steering queue (seen at the next tool boundary, turn not cancelled); with no running turn it returns `-32002` and queues nothing. `mode: \"record\"` with `{question, answer, forkSessionId?}` instead appends a `side_query` custom transcript entry that is never sent to the LLM, with or without a running turn (see `docs\u002Facp-steer.md`).\n\n## Configuration Hierarchy\n\nSettings merge in order (later wins):\n\n1. **Global config** — `\u003Cagent-dir>\u002Fsettings.json` (`~\u002F.config\u002Ffir` by default; override with `FIR_AGENT_DIR` or `fir --agent-dir \u003Cdir>`, with the CLI flag winning)\n2. **Project config** — `.fir\u002Fsettings.json` in project root\n3. **CLI flags** — override everything\n\nAll settings fields are optional. Project settings are merged on top of global settings field-by-field; nested objects merge recursively, arrays and primitives from the override win.\n\n### Global Directory (`\u003Cagent-dir>\u002F`, default `~\u002F.config\u002Ffir\u002F`)\n\n| File | Purpose |\n|------|---------|\n| `settings.json` | Default provider, model, thinking, compaction, retry, terminal, image settings |\n| `models.json` | Add or override models per provider (custom ids, base URLs, headers, costs, sweScore) |\n| `provider-usage.json` | Usage\u002Fauth warnings: `enabled`, `warn_levels` ([80,95]), `stale_after_seconds` (600), `refresh_grace_minutes` (15), `login_max_age_days` (0=off), `burst_points` (3), `burst_window_seconds` (600) — burst = account-wide 5h utilization jump read from response headers |\n| `keybindings.json` | Custom key bindings for interactive mode |\n| `sessions\u002F` | Saved conversation sessions |\n| `skills\u002F` | User-level skills (shared across projects) |\n| `extensions\u002F` | User-level extensions (shared across projects) |\n| `packages\u002F` | Installed external packages (user scope) |\n\n### Project Directory (`.fir\u002F` in project root)\n\n| Path | Purpose |\n|------|---------|\n| `settings.json` | Project-level setting overrides |\n| `keybindings.json` | Project-level keybinding overrides (merged on top of global) |\n| `skills\u002F` | Project-specific skills (auto-discovered) |\n| `extensions\u002F` | Project-specific extensions (auto-discovered) |\n| `packages\u002F` | Installed external packages (project scope, via `--local`) |\n| `mcp.json` | MCP server configuration (project-level) |\n\n### Project Context Files\n\nfir reads `AGENTS.md` (or `CLAUDE.md`) from the working directory and its ancestors, automatically including them in the system prompt. This is the primary way to give fir project-specific instructions.\n\n## Authentication\n\nTwo methods:\n\n1. **Environment variables** — set `\u003CPROVIDER>_API_KEY` (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). See the full list below.\n2. **OAuth** — use `\u002Flogin` in interactive mode, or `fir login \u003Cprovider-id>` from the shell. Supports Anthropic, Google Antigravity, OpenAI Codex, GitHub Copilot, and Poe. Credentials are persisted automatically.\n3. New OAuth providers can be added as extensions\n\n## Login subcommand\n\n`fir login \u003Cprovider-id>` runs the OAuth flow for a provider from the shell (no interactive TUI required) and writes credentials to `\u003Cagent-dir>\u002Fauth.json`. Because OAuth providers are contributed by auth extensions, `fir login` loads extensions before running the flow — honouring the `settings.json` `extensions` allowlist, `-e\u002F--extension`, `-d\u002F--disable-extension`, and `--no-extensions`, exactly like the main CLI. `fir login list` (or bare `fir login`) lists every provider registered after extensions load. Equivalent to the long-standing `--login \u003Cprovider-id>` flag; both paths share the same session-bootstrap code.\n\n### Multiple accounts per provider\n\nA provider can hold several accounts at once. Running `fir login \u003Cprovider-id>` a second time **adds** another account rather than evicting the first — e.g. a personal and a work Anthropic login coexist. Accounts are stored in `auth.json` under composite slot keys: the bare `\u003Cprovider>` key is the default account, and extra accounts use `\u003Cprovider>#\u003CaccountId>`. For Anthropic the account id is a **human-readable** slug built from the email and organization name (e.g. `anthropic#me@example.com-acme-corp`, falling back to uuids only when neither is available), and the selector label shows the display name (or email) plus the org — e.g. `Ada Lovelace (Acme Corp)`. The same user logged into two different orgs therefore yields two distinct, recognisable accounts. Older fir builds reading the file still find the default account and ignore the `#account` keys.\n\nEach account appears as its own labelled group in the model selector and `--list-models` (e.g. \"Anthropic (Claude Pro\u002FMax) (work@x.com)\"), so switching accounts live is just selecting a model under that group — no env juggling, no restart. `fir login list` also prints the stored accounts. Remove a single account with `fir logout \u003Cprovider[#account]>` (or `fir logout list` to see slots).\n\n### Keeping an OAuth login alive on an idle machine\n\n`fir auth refresh [provider-id | provider-id#account]` performs the OAuth refresh grant and nothing else — no model, no tools, no inference, no quota. It exists because fir otherwise only refreshes a token as a side effect of running a turn, so a box that sits idle (a bot host, a rarely-used fleet machine) eventually lets its *refresh* token expire and needs an interactive browser re-login. Put it in cron:\n\n```\n0 4 * * *  fir auth refresh\n```\n\n- Provider defaults to `anthropic`; any registered OAuth provider id works. Nothing provider-specific is hardcoded — endpoints, client id and expiry normalisation come from the extension's `declare_oauth_provider` spec and `auth_post_exchange` hook.\n- Every stored account slot of that provider is refreshed, default first. One slot failing does not stop the others, and the exit status is non-zero if any failed.\n- Pass an **account slot key** (`fir auth refresh anthropic#work@x.com`) to refresh just that one account. This is the same `provider#account` form this command prints, and the one `fir login list` and `fir logout` accept, so a slot key pasted straight back in works. An unknown slot under a valid provider lists the provider's stored slot keys instead of refreshing anything.\n- Output is one greppable tab-separated line per slot: `\u003Cslot> \u003Clabel> \u003Coutcome> expires=\u003CRFC3339> in=\u003Cduration>`, where outcome is `refreshed`, `fresh`, `skipped` or `failed`.\n- A credential more than an hour from expiry is left alone rather than needlessly rotated. `--within DURATION` moves that guard; `--force` rotates regardless.\n\n**Rotation is destructive — know this before you use `--force`.** OAuth refresh tokens rotate on every grant, and Anthropic **revokes the previous access token the instant the rotation happens** — it does not survive to its stated `expires`. fir handles this: any process resolving a key re-reads `auth.json` when it has changed on disk, and the request path re-reads unconditionally before reporting an auth failure, so a session that is live across a rotation picks up the new token and retries rather than dying on a `401 OAuth access token has been revoked`. Never hand-write `auth.json` or run a refresh grant outside fir — an out-of-tree writer racing fir over a rotating credential can replay a consumed refresh token and get the entire token family revoked.\n\n> **Copying the agent dir does NOT isolate a `--force` test.**\n> `cp -a ~\u002F.config\u002Ffir \u002Ftmp\u002Ffirtest && FIR_AGENT_DIR=\u002Ftmp\u002Ffirtest fir auth refresh --force`\n> looks safe and is not. The copy duplicates the *file*, but the refresh token inside it is\n> the same credential upstream — forcing a grant on the copy rotates the real credential at\n> the provider and revokes the access token every live fir session is holding. A copied\n> agent dir is only safe for paths that spend no grant (a plain `fir auth refresh` that\n> reports `fresh`, `skipped`, or an unknown-provider error). Genuinely isolated `--force`\n> testing needs a **separate login**, not a file copy.\n\n\n### Secret storage (`auth.storage`, `fir auth storage`)\n\nBy default every credential (provider logins, API keys, MCP OAuth tokens) is plain text in `auth.json` (0600, flock-guarded). Set `\"auth\": {\"storage\": \"auto\"}` (or `\"keyring\"`) in the **global** `settings.json` to move secrets into the OS secret store — macOS Keychain, Linux Secret Service (GNOME Keyring\u002FKWallet over D-Bus), Windows Credential Manager. `auth.json` then keeps only metadata (type, label, expiry) plus a `_fir_storage` marker; secrets are one keychain item per slot under service `fir`. `auto` falls back to `auth.json` when no store answers (e.g. headless Linux without a session bus); `keyring` errors instead. Once migrated the marker is authoritative: if the store later becomes unavailable fir reports an error rather than silently writing secrets back to the file.\n\n- `fir auth storage` — show where secrets live, the setting, and whether the OS store answers.\n- `fir auth storage keyring` — migrate now (each secret is written and read back before the file is stripped).\n- `fir auth storage file` — move secrets back into `auth.json` and delete the keychain items (set `auth.storage` to `file` first, or the next access migrates them again).\n\n### Amazon Bedrock accounts\n\n`fir login bedrock` configures one or more Bedrock accounts (these are credential configs, not OAuth flows). Three modes:\n\n- **iam-profile** — a named `~\u002F.aws` profile (SigV4). Profiles are seeded from `~\u002F.aws\u002Fconfig`.\n- **iam-keys** — explicit AWS access\u002Fsecret (and optional session) keys (SigV4).\n- **bearer** — a Bedrock API key \u002F bearer token (suppresses SigV4); seeded from `$AWS_BEARER_TOKEN_BEDROCK` if present.\n\nWith no flags it prompts interactively; non-interactive flags: `--mode`, `--account NAME`, `--region`, `--profile`, `--access-key`, `--secret-key`, `--session-token`, `--token`. Each account can carry a per-account `region` (which selects the regional `bedrock-runtime` endpoint) and per-model inference-profile ARN overrides (stored in the credential's `extra.modelOverrides`). Like OAuth accounts, multiple Bedrock accounts coexist and switch live via the selector; the credential type is stored as `aws_iam` (profile\u002Fkeys) or `api_key` (bearer) in `auth.json`.\n\n## Session observation (`fir observe` \u002F `fir send`)\n\nEvery fir session writes a JSONL transcript to disk via `SessionStore`. The builtin `observe` extension registers a sidecar at `$XDG_STATE_HOME\u002Ffir\u002Fagents\u002F\u003Csession-id>.json` containing discovery metadata (pid, socket path, transcript path, cwd, status, name) and listens on a Unix socket for input injection.\n\n`fir observe` and `fir send` are **CLI verbs of the `observe` extension** (declared via `cli_verbs:` in its frontmatter), dispatched cold by fir's verb-dispatcher. There is no Go subcommand for either — the entire implementation lives in `pkg\u002Fresources\u002Fbuiltin_extensions\u002Fobserve.py`. See `docs\u002Fdesign\u002Fextension-cli-verbs.md` for the mechanism.\n\nThe same extension also exposes:\n- **`\u002Fobserve`** and **`\u002Fsend`** slash commands — usable from inside an interactive session for snapshot inspection \u002F one-shot message injection.\n- **`observe_session`**, **`send_session`**, and **`stop_session`** AI tools — let the agent inspect, nudge, or terminate sibling sessions as part of a turn.\n\nSlash commands and tools take **snapshots** (last N transcript lines) and send single messages; they do not live-tail. Use `fir observe \u003Cid>` from another terminal for live observation. `observe_session` \u002F `\u002Fobserve` also accept a partial-range slice (`start`\u002F`end` tool params, or `--start=`\u002F`--end=` flags) to page through a long transcript by line number instead of pulling the tail.\n\nThe `observe` extension is a builtin and loads automatically in every session — no special flags needed. Observation commands run from any other terminal:\n\n```\nfir observe                        # list LIVE sessions (status idle\u002Frunning\u002Ferror\u002Fno-model\u002Fobserve-lost)\nfir observe --all                  # include ended + crashed sessions\nfir observe --json                 # the list as JSON (one status object per session)\nfir observe \u003Cid-prefix>            # status + last 50 entries, then exit (on a TTY: follow live)\nfir observe \u003Cid> -n 200            # snapshot of the last 200 entries; always exits\nfir observe \u003Cid> -f                # follow live until the session ends (Ctrl-C \u002F Ctrl-\\ to stop)\nfir observe \u003Cid> --status [--json] # status only: idle|running|error|no-model|observe-lost|ended|crashed,\n                                   #   model, current tool, run count, last error, startup notice\n                                   #   crashed = fir host dead; observe-lost = host alive but\n                                   #   not observable\u002Fsendable in real time (do not kill for it)\nfir observe \u003Cid> --wait [--timeout S]  # block until the current run ends (124 on timeout)\nfir observe \u003Cid-prefix> --json     # raw JSONL (snapshot; with -f a raw tail)\nfir observe --cwd .                # resolve by directory (symlinks resolved)\nfir observe \u003Cid> --interact        # follow + pipe stdin to the session\n\nfir send \u003Cid> 'message'            # send one message (args joined)\nfir send \u003Cid> --wait 'message'     # send, block until that turn ends, print the final reply\n                                   #   (exit 1 if the turn failed, 124 on --timeout S)\necho \"brief\" | fir send \u003Cid>       # piped stdin = ONE message (multi-line ok)\nfir send \u003Cid-prefix>               # on a TTY: interactive, Enter sends each line\nfir send \u003Cid> --steer|--follow ... # deliver as steer (interrupt) \u002F followUp (queue)\nfir send \u003Cid> --abort              # cancel the current turn\nfir send \u003Cid> --model prov\u002Fid ...  # switch provider+model (alone, or before the message)\n```\n\n`\u003Cid-prefix>` prefix-matches against session id, session name, or basename(cwd). First-line sigils in `fir send`: `!message` → steer, `+message` → followUp, `~` → abort, `\\!`\u002F`\\+`\u002F`\\~` to escape. `Ctrl-\\` detaches. In interactive `fir send` and `fir observe --interact`, a typed `\u002Fmodel provider\u002Fid` line switches the session's model instead of being sent. The `send_session` tool takes a `model` parameter for the same switch.\nA steer lands at the next turn boundary: most tools (bash) finish first, but `wait` yields within ~1s (`outcome=interrupted` + resume handle) and `pipe` stops between steps.\n\nStatus comes from the core-owned **`session\u002Fstatus` observable card** (`\u003Ctranscript>.cards`), which `AgentSession` writes in agent-loop order on every lifecycle transition (`pkg\u002Fsession\u002Fstatuscard.go`): `idle`, `running` (from prompt acceptance to `agent_end`; detail names the running tool), `error` (last run ended with a provider\u002Fauth error — the message is in `error:`), `no-model` (prompts are refused; the startup \"No models available…\" notice is in `notice:`). Detail is `key: value` lines (`status`, `model`, `tool`, `runs`, `error`, `notice`). Liveness additionally probes the session's input socket, so a reused pid does not resurrect a dead session.\n\n**Note**: when using `-e \u003Cname>` flags to selectively enable extensions, include all required auth extensions too (e.g. `-d auto-namer -d notify` to suppress noise without blocking auth). Using `-e observe` alone blocks auth extensions.\n\nFor multi-pane monitoring: run `fir observe \u003Cid> -f` in one tmux pane and `fir send \u003Cid>` in another.\n\n## Session stores — reading other hosts' sessions (`sessionStores`)\n\nfir writes sessions only to its own store, `\u003Cagent-dir>\u002Fsessions\u002F` (label `local`). Extra **read-only** stores — directories laid out the same way, e.g. a Syncthing copy of another host's `~\u002F.config\u002Ffir\u002Fsessions\u002F` — are configured in the GLOBAL `settings.json` (a project `.fir\u002Fsettings.json` cannot add stores; fir warns and ignores it):\n\n```json\n{ \"sessionStores\": [{ \"path\": \"~\u002Ffir-traces\u002Fzbox\", \"label\": \"zbox\" }] }\n```\n\n`path` must be absolute or `~\u002F…`; `label` defaults to the directory name, must match `[A-Za-z0-9._-]+`, be unique and not `local`. Stores are always read-only (`readOnly: false` is ignored with a warning). A missing directory is skipped, not an error; a path that overlaps fir's own `\u003Cagent-dir>\u002Fsessions\u002F` is dropped with a warning. `FIR_SESSION_STORES=zbox=~\u002Ffir-traces\u002Fzbox[,label=path]` replaces the setting for one run.\n\nWhat sees them:\n- `fir sessions list [--all|--cwd DIR] [--store L|--read-only] [--limit N] [--json]` — TSV `store modified id msgs path cwd name first_message`, newest first, no header (`--json`: one object per line). Archive sessions are listed from each transcript's `.meta.json` only (no `.meta.json`, or modified in the last minute = mid-sync, → skipped); a missing cwd is decoded from the directory name (lossy: `-` vs `\u002F`).\n- `fir sessions dirs [--json]` — `store path readonly exists`.\n- `fir sessions search PATTERN [--kind bookmark|transcript] [--cwd DIR] [--store L] [--since 72h] [--limit 50] [--per-file 3] [-i] [--json]` — Go regexp, bookmarks first then transcripts, newest first (ctx-* logs excluded); hit = `store kind id path:line snippet`. Bookmarks are fast; a whole archive's transcripts take tens of seconds.\n- `\u002Fresume` picker (labelled `[zbox]`), `fir --resume \u003Cid|label:id>`, ACP `session\u002Flist` (title prefixed `[label]`, `_meta: {\"fir\u002Fstore\": label, \"fir\u002FreadOnly\": true}`), ACP `session\u002Fresume`\u002F`load` (paths inside a configured store are accepted; anything outside every store is still rejected), ACP `\u002Fresume`, and `observe_session all=true` \u002F `fir observe --all` (newest 20 as `archived:\u003Clabel>`; `fir observe \u003Cid|label:id>` snapshots an archived transcript).\n\nResuming an archive session **forks** it into the local store (message: \"Session is in read-only store … forked\"); fir never locks, repairs, writes state, cards, sidecars or metadata inside a read-only store — the check lives in the session store itself. The `recall` skill searches every store through `fir sessions`.\n\n## Driving a fir agent on another host\n\n`observe_session` \u002F `send_session` \u002F `stop_session` see only sessions on the local machine. For a session on another host, run the CLI there over ssh (`rexec`) — snapshots need no TTY and always exit:\n\n```\n# start a named, headless-ish agent (tmux is just the process holder)\nssh HOST \"tmux new -d -s w1 -c ~\u002Fproj 'fir --session-name w1'\"\n\nssh HOST fir observe                       # what is running there, with status\nssh HOST fir observe w1 --status --json    # machine-readable status \u002F last error\nssh HOST fir send w1 --wait --timeout 30m \u003C brief.md  # send, wait for the turn, get the reply\nssh HOST fir observe w1 -n 40              # what it did\nssh HOST \"fir send w1 '!new direction'\"    # steer;  fir send w1 --abort  cancels\n```\n\nScraping the tmux screen is a fallback only. For a long-lived human view: `ssh -t HOST 'tmux new -As watch \"fir observe \u003Cid> -f\"'`.\n\n## Environment Variables\n\n{{FIR_ENV_VARS_TABLE}}\n\n## Adding Custom Models\n\nfir's model list is the union of built-in models and anything you add. Three ways to add a model so it shows up in `\u002Fmodel`, `--list-models`, and is selectable as a default:\n\n### 1. `models.json` (preferred — persistent, visible everywhere)\n\nDrop a `models.json` in `~\u002F.config\u002Ffir\u002F` (global) or `.fir\u002F` (project). Add models under the relevant provider — they appear alongside built-ins:\n\n```json\n{\n  \"providers\": {\n    \"amazon-bedrock\": {\n      \"models\": [\n        {\n          \"id\": \"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile\u002Fabc\",\n          \"name\": \"Opus 4.6 (Bedrock app profile)\",\n          \"api\": \"bedrock-converse-stream\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 200000,\n          \"maxTokens\": 8192,\n          \"sweScore\": 73.5\n        }\n      ]\n    }\n  }\n}\n```\n\nThe same file also supports per-provider overrides (`baseUrl`, `apiKey`, `headers`, `authHeader`) and per-model overrides under `modelOverrides`. Most model fields (cost, `contextWindow`, `maxTokens`, `sweScore`, `compaction`, `serverTools`, …) can be set on a custom model or supplied via `modelOverrides` to patch a built-in. `sweScore` (SWE-bench Verified %, 0–100) shows up in the `\u002Fmodel` selector and drives its sort order.\n\n#### `api` — the wire protocol (and its silent failure mode)\n\n`api` selects the **wire protocol**, not the vendor. Legal values are the `API`\nconstants in the external module `github.com\u002Fkfet\u002Fai` (`types.go`, at the\nversion pinned in fir's `go.mod`; re-check with\n`grep 'API =' \"$(go env GOMODCACHE)\"\u002Fgithub.com\u002Fkfet\u002Fai@*\u002Ftypes.go`, aliased in\nfir as `pkg\u002Fai\u002Faliases.go`):\n\n`openai-completions`, `openai-responses`, `azure-openai-responses`,\n`openai-codex-responses`, `anthropic-messages`, `bedrock-converse-stream`,\n`google-generative-ai`, `google-vertex`.\n\nExtensions may register further wire protocols at runtime (`ext-api:*`, `ext:*`).\n\nOpenAI-compatible endpoints (OpenRouter, Groq, local servers, …) take\n`openai-completions` — **not** `\"openai\"`, which is a provider name, not an API.\n\nNeither mistake is caught when the config loads:\n\n- **Unknown `api`** — the model still appears in `--list-models` and `\u002Fmodel`;\n  every request to it fails with `no API provider registered for api: \u003Cvalue>`.\n- **Missing `api`** on a provider fir has no built-in for — validation rejects\n  the *whole* custom-model load (`Provider X, model Y: no \"api\" specified. Set\n  at provider or model level`), so every custom model disappears and\n  `--provider X` reports `Unknown provider \"X\"`. That error text is shown only\n  in the interactive `\u002Fmodel` selector, not by `--list-models`.\n\n`api` can be set per provider and per model: model-level wins, then\nprovider-level, then — for built-in providers only — the API of that provider's\nfirst built-in model. So a custom model on a built-in provider may omit `api`; a\nmodel on a brand-new provider may not.\n\n#### Unix-socket endpoints (`unix:\u002F\u002F`)\n\nA `baseUrl` may use the `unix:\u002F\u002F` scheme to reach an HTTP endpoint over a unix\ndomain socket, e.g. `unix:\u002F\u002F\u002Frun\u002Fboxres\u002Fbifrost.sock\u002Fanthropic`. Rule: the\nsocket path is the URL path up to and including the **first** path segment\nending in `.sock` (`\u002Frun\u002Fboxres\u002Fbifrost.sock`); the rest (`\u002Fanthropic`) is the\nHTTP path, sent with `Host: localhost` (so requests go to\n`\u002Fanthropic\u002Fv1\u002Fmessages`). A `unix:\u002F\u002F` URL with no `.sock` segment is an error.\nWorks for every HTTP-based API (anthropic-messages, openai-completions, …),\nstreaming included.\n\nA custom provider may **omit `apiKey`** when every model's effective `baseUrl`\nis `unix:\u002F\u002F` — access is controlled by socket permissions\u002Fpeer credentials, and\nfir sends no auth header:\n\n```json\n{ \"providers\": { \"bifrost\": {\n    \"baseUrl\": \"unix:\u002F\u002F\u002Frun\u002Fboxres\u002Fbifrost.sock\u002Fanthropic\",\n    \"api\": \"anthropic-messages\",\n    \"models\": [{ \"id\": \"claude-sonnet-4-6\" }] } } }\n```\n\n`apiKey` and header values are resolved the same way: a value starting with `!`\nis run as a shell command and its stdout used; otherwise the value is used as an\nenv var *name* if that variable is set, else taken literally. There is no\n`${VAR}` interpolation inside a string.\n\n#### Provider routing: OpenRouter and Vercel AI Gateway\n\nAggregators route one model id to whichever backend provider they pick. Those\nbackends are **not interchangeable**: they run different serving stacks, so the\nsame id can vary in vision support, reasoning-effort handling, quantization,\ncontext window and latency between requests. If reproducibility matters, pin\nthe backend rather than trusting the default \"best available\" routing.\n\nBoth aggregators are configured through the per-model `compat` block — either\non a custom model under `models`, or on a built-in via `modelOverrides`. It is\na **model-level** key: there is no provider-level `compat`. Routing is emitted\n**only** when that model's `baseUrl` contains `openrouter.ai` \u002F\n`ai-gateway.vercel.sh` respectively, so a stray key elsewhere is inert, not an\nerror.\n\n```jsonc\n{ \"providers\": { \"openrouter\": {\n  \"modelOverrides\": {\n    \"moonshotai\u002Fkimi-k2.5\": {\n      \"compat\": {\n        \"openRouterRouting\": {\n          \"only\":  [\"moonshot\"],            \u002F\u002F exclusive allow-list\n          \"order\": [\"moonshot\", \"together\"] \u002F\u002F try in this sequence\n        }\n      }\n    }\n  } } } }\n```\n\n- `only` — providers exclusively allowed. This is the reproducibility knob.\n- `order` — ordered preference; other providers may still serve as fallback.\n- `ignore` — providers to skip.\n- `allowFallbacks` (default true) — set `false` to fail rather than silently\n  route to a backend outside `only`\u002F`order`.\n- `requireParameters` — only route to providers supporting every parameter in\n  the request (the vision \u002F reasoning-effort mismatch guard).\n- `zdr` — restrict to Zero Data Retention endpoints. `dataCollection`\n  (`\"allow\"` | `\"deny\"`) and `enforceDistillableText` are the related\n  privacy filters.\n- `quantizations` — e.g. `[\"fp8\"]`, to exclude aggressively quantized serves.\n- `sort` — `\"price\"` | `\"throughput\"` | `\"latency\"`, or an object with\n  `by`\u002F`partition`.\n- `maxPrice` — `{prompt, completion, image, audio, request}` USD per MTok caps.\n- `preferredMinThroughput` \u002F `preferredMaxLatency` — a number, or an object of\n  `p50`\u002F`p75`\u002F`p90`\u002F`p99` cutoffs.\n\nKey names are **camelCase** in fir config and are translated to OpenRouter's\nsnake_case on the wire (`allowFallbacks` → `allow_fallbacks`). Fields whose\nOpenRouter schema is a union of a scalar and an object (`sort`,\n`preferredMinThroughput`, `preferredMaxLatency`) are forwarded verbatim without\nlocal validation — a malformed value is rejected by the API, not by fir.\n\nVercel AI Gateway uses `vercelGatewayRouting` with `only` \u002F `order` only; it is\nemitted as `providerOptions.gateway`.\n\nDiscover valid provider slugs for a model with OpenRouter's endpoints API:\n\n```bash\ncurl -s https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fmodels\u002Fmoonshotai\u002Fkimi-k2.5\u002Fendpoints \\\n  | jq -r '.data.endpoints[] | \"\\(.provider_name)\\t\\(.context_length)\\t\\(.quantization)\"'\n```\n\n**Current limit:** none of these are validated locally — an unknown provider\nslug simply routes to nothing and OpenRouter answers with an error.\n\n#### Drop-in fragments: `models.d\u002F`\n\nAlongside `models.json`, fir reads every `*.json` file in a `models.d\u002F`\ndirectory next to it — `~\u002F.config\u002Ffir\u002Fmodels.d\u002F` (global) and\n`.fir\u002Fmodels.d\u002F` (project). Each fragment has the **same shape** as\n`models.json`. This lets tools and agents add a model or provider by\ndropping in a self-contained file, with no edits to a shared monolith.\n\nLoad order and merge semantics:\n\n- `models.json` is the **base layer**; fragments are deep-merged on top in\n  **lexical filename order** — use `NN-` prefixes (e.g. `10-fable.json`,\n  `20-internal.json`) to control precedence.\n- Within a provider: scalar fields (`baseUrl`, `apiKey`, `api`) and\n  `authHeader` are overwritten when a later layer sets them; `headers` and\n  `modelOverrides` maps are key-merged; the `models` array is concatenated\n  and **de-duplicated by `id`, last writer wins**.\n- A fragment alone is sufficient — `models.json` need not exist.\n- A malformed or `providers`-less fragment surfaces a load error (visible via\n  the registry error \u002F `--list-models`), it is not silently skipped.\n\n```jsonc\n\u002F\u002F ~\u002F.config\u002Ffir\u002Fmodels.d\u002F10-fable.json\n{ \"providers\": { \"anthropic\": { \"models\": [\n  { \"id\": \"claude-fable-5\", \"name\": \"Claude Fable 5\",\n    \"contextWindow\": 200000, \"maxTokens\": 64000 } ] } } }\n```\n\nThe catalog is read at process start, and re-read on `\u002Freload` (which now\nalso refreshes the model registry) and when the model selector opens. So a\nnewly added fragment is picked up by `\u002Freload` in a running session, by the\nnext `fir` process, or — for the Poe relay's dropdown — after a relay\nrestart (the relay snapshots the catalog once at its own startup).\n\n#### Catalog overlay (fir-dist)\n\nBelow your config, above the compiled-in catalog, fir merges a **catalog\noverlay**: a `models.d`-shaped document published as a static file at\n`https:\u002F\u002Fraw.githubusercontent.com\u002Fkfet\u002Ffir-dist\u002Fmain\u002Fcatalog-v1.json`. It\nexists so a newly-released model can reach the whole fleet as *data* — no\nbinary release, no redeploy. It can add models, correct metadata, and move a\nprovider's default model (`providerDefaults`). A provider default is only a\nproduct-line anchor: it resolves to the newest registered generation of the\nsame line (e.g. `claude-opus-5` → `claude-opus-5-5` once that model is\nregistered), keeping prefixes, variants and `-preview` tags, skipping dated\nsnapshots. Explicit pins (`defaultModel`, `--model`) are not advanced.\n\nFull precedence, lowest to highest:\n\n```\ncompiled-in catalog  \u003C  fir-dist overlay  \u003C  models.json  \u003C  models.d\u002F*.json\n```\n\n**Your config always wins.** The overlay may not set `apiKey`, `baseUrl`,\n`headers` or `authHeader` at any level (provider, model, or `modelOverrides`) —\nendpoint and auth stay under operator control, so a typo in a published catalog\ncan never redirect the fleet's credentials. It follows that the overlay can only\nadd models to providers fir already knows about.\n\n- Fetched at startup and then every hour for the life of the session, cached\n  at `\u003Cagent-dir>\u002Fcache\u002Fcatalog-v1.json`; a changed catalog is hot-applied, so\n  even a bot process that has been up for weeks follows within the TTL.\n  A binary also embeds a snapshot, so fir works fully offline; every failure\n  (no network, 404, malformed JSON, unknown schema version) is soft and logged\n  at debug, never a degradation.\n- Force a refresh with `\u002Freload`, which refetches ignoring the TTL and\n  hot-applies a changed catalog to the running session.\n- `fir --list-models --verbose` prints each model's origin\n  (`builtin` \u002F `overlay` \u002F `user:models.json` \u002F `user:models.d\u002F\u003Cfile>`) so you\n  can see which layer won.\n\n**Client version pins (`clientVersions`).** The overlay also carries a small,\nclosed set of third-party client version scalars — currently just\n`clientVersions.claudeCode`, the Claude Code CLI version the `anthropic-auth`\nextension advertises as `claude-cli\u002F\u003Cv> (external, cli)`. Anthropic gates new\nmodels on it, so shipping it as data means a gated model and the version pin it\nrequires arrive together in one document, instead of costing a one-line binary\nrelease.\n\n- The extension holds **no version literal**: it declares the template\n  `claude-cli\u002F{clientVersion.claudeCode} (external, cli)` and fir expands\n  `{clientVersion.\u003Ckey>}` in header **values only** (never names, never URLs)\n  at token-request time and after `auth\u002Fmodify_models`. Hot: a published\n  change takes effect within a TTL with no restart.\n- Values must match `^[0-9]+(\\.[0-9]+){0,3}$` and be ≤ 32 bytes, so nothing\n  free-form can ride in.\n- The value compiled into the binary is a **floor**: a published document can\n  only ever *advance* the pin, never roll a host below what it shipped with.\n- To read what a session is advertising: `\u002Freload` names it in its status line\n  (`… (claude-code 2.1.280)`); the committed value is\n  `clientVersions.claudeCode` in `pkg\u002Fmodels\u002Fcatalog-v1.json`.\n- The nightly `model-watch` job reads npm `dist-tags.latest` for\n  `@anthropic-ai\u002Fclaude-code` and proposes a forward-only bump on the same\n  rolling PR as new models; a moved pin alone is enough to open one.\n- **Detecting that a bump is needed.** When an OAuth Anthropic request is\n  rejected with the vendor's gate sentence (`Claude Code X does not support\n  this model; version Y or newer is required`), the error is rewritten to name\n  the pin, its source (overlay vs embedded floor) and the remedy, and a\n  `client-version-gate` record (pin, pinSource, required, model, host, ts,\n  vendor error) is appended to `~\u002F.config\u002Ffir\u002Fdoctor.jsonl` —\n  `doctor_query pattern=client-version-gate` lists them. Every session start\n  prints a one-line warning while any record is unresolved; it clears itself\n  once the effective pin moves strictly past the rejected one (no manual\n  step). `fir doctor client-version-gates` prints the unresolved ones (nothing\n  when clear); fleet converge publishes that as\n  `~\u002Fsync\u002Fshared\u002Ffleet\u002Fstatus\u002F\u003Chost>.gates`.\n\nEscape hatches (env vars):\n\n| Variable | Effect |\n| --- | --- |\n| `FIR_NO_CATALOG_OVERLAY=1` | Disable overlay **fetching**. The embedded snapshot still applies — this pins a host to the catalog its binary shipped with. |\n| `FIR_CATALOG_OVERLAY_URL=\u003Curl>` | Fetch the overlay from somewhere else (staging file, local server). |\n\nPublishing: edit `pkg\u002Fmodels\u002Fcatalog-v1.json` in the fir repo and merge to\n`main`; the `publish-catalog` workflow pushes it to fir-dist. **To roll a\ncatalog change back, re-publish the old content with a fresh `generatedAt`** —\na binary loads whichever of its embedded snapshot and its cache is newer, so\nreverting the commit alone can be a no-op.\n\n### 2. Environment variables (Claude Code parity)\n\nSet `CLAUDE_CODE_USE_BEDROCK=1` and `ANTHROPIC_MODEL=\u003Cid-or-arn>` (plus standard AWS auth — `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`\u002F`AWS_SECRET_ACCESS_KEY`, optionally `AWS_REGION`). At startup fir registers the model under `amazon-bedrock` automatically so it appears in `\u002Fmodel`. Explicit `--provider`\u002F`--model` flags still win.\n\n`ANTHROPIC_MODEL` accepts a regular Bedrock model id (e.g. `us.anthropic.claude-opus-4-6-v1`) or a full Bedrock ARN (e.g. `arn:aws:bedrock:us-east-1:123:application-inference-profile\u002Fabc`).\n\n### 3. CLI one-off\n\n`fir --provider amazon-bedrock --model \u003Cid-or-arn>` works without any config — fir clones a sibling Bedrock model, substitutes the id, and routes through `ConverseStream`. Not persisted; not shown in `\u002Fmodel` until restart with `models.json` or env vars.\n\n## Extensions\n\nExtensions are standalone scripts (Python, shell, etc.) in `.fir\u002Fextensions\u002F` (project) or `\u003Cagent-dir>\u002Fextensions\u002F` (global). They communicate with fir over JSON-RPC 2.0 on stdio and can register custom tools, slash commands, and event handlers.\n\nA Python SDK is provided (`fir_ext.py`). No code changes needed to add an extension — just drop a script in the directory.\n\nDisable an extension with `chmod -x`; a renamed file still loads.\n\nControl loading via `settings.json` `\"extensions\"` allowlist, `--extension`\u002F`-e` flags, or `--no-extensions`.\n\nExtensions can optionally self-restrict by mode using script comment frontmatter (`# modes: ...`), e.g. `tui`, `acp`, or `text`. Mark an extension `# explicit: true` to make it **opt-in only** — it stays discoverable (so `fir -e \u003Cname>` and listings find it) but is **not** auto-loaded; it runs only when named in the allowlist. The shipped `demo` and `hello` example extensions use this.\n\nBuiltin extensions (notify, tmuxspinner, plan_nudger, handoff_nudger, etc.) are embedded in the binary and auto-discovered at lowest priority. Use `fir extensions` to list them and `fir extensions install \u003Cname>` to extract one for customisation.\n\n`handoff-nudger` watches context usage after each turn and prepends a `[SYS_EXT]` note telling you to wrap up and `self_handoff` — auto-compaction only fires at 70% and is lossy, while cache-read cost grows linearly with conversation length. Two triggers: the ceiling, the lower of `atTokens` (500000) \u002F `atPercent` (65) of the window; and a cold prompt cache — 30% of that ceiling (floored at 100000) once the session has been idle past `idleMinutes` (65), since the provider's 1h cache has expired by then and every further turn pays full input on the whole prefix. Tune or disable it in `handoff-nudger.json` (`atTokens`, `atPercent`, `idleMinutes`, `nudgeEvery`, `off`) in `.fir\u002F` or `~\u002F.config\u002Ffir\u002F`.\n\n**Context provenance (`ctx`)** — while the builtin `ctx` extension runs, fir core logs every chunk of each provider request to `ctx-\u003Csession-id>.jsonl` next to the transcript (`kind:src` rows `{turn, addr, source, owner, tokens, hash}` on first sight; one `kind:turn` row per call with `model`, `tokens` and present `addrs`; tokens ≈ bytes\u002F4). Sources: system prompt, AGENTS.md, SYS_EXT section, SYS_EXT, skills index, tool schema, lesson, skill read, tool result, user msg. Chunks ≥100 tokens (and every lesson) carry a stable `[ctx:\u003C5-hex>]` tag (tool results: `[ctx: …]` meta) on the provider copy only — history is untouched and tags are content-derived, so caches hold. The model may call `ctx_used(addrs, why?)` to log chunks it used (`kind:cite` rows). The `ctx` observable card shows tokens · sources · cited (`observe_session ext=ctx`: cited, costliest idle, duplicates). `\u002Fctx [model]` reports across sessions per model sorted by cost. After `reeval_turns` (default 300; `ctx.json` or `FIR_CTX_REEVAL_TURNS`, 0 = off) new turns per model it injects a review prompt asking for trim\u002Fdedupe proposals as a diff — never applied; `pinned` patterns (default system prompt, auth, safety, security, format, copyright) are marked non-trimmable; per-model scores in the global `ctx-state.json` (never the project `.fir\u002F`). Disable with `--extension`\u002Fdisabled-extensions like any extension.\n\n**Extension sections** — each extension may own one persistent section of standing context (`ctx.set_section(text)` \u002F `ctx.clear_section()`), stored by fir at `\u003Cagent-dir>\u002Fsections\u002F\u003Cext-name>.md`. Fir reads that directory itself before the first turn (so `fir -p` sees sections without waiting on extension events) and injects the sections of installed, enabled and trusted extensions as one `[SYS_EXT sections]` message right after the system prompt (cache breakpoint after it; no timestamps, byte-stable). Mid-session changes are re-injected in full as `[SYS_EXT section=\u003Cname> vN replaces vM]` on the next turn. Caps: ~500 tokens per section, ~2000 total; over-cap writes are rejected, never truncated. Disabled extensions keep their file (not emitted); uninstalled extensions' sections are deleted at startup. `\u002Fsections` shows what is injected. The builtin `mood` extension publishes its lessons this way. Fir core also owns one per-session section, `[section=mcp]` (never written to disk): one line per configured MCP server that is unhealthy (needs_auth, token_expired, error, disabled) with the fir command that fixes it, e.g. `MCP server slack: configured, NOT AUTHENTICATED, tools unavailable -> fir command: \u002Fmcp login slack`. It is empty when all servers are healthy; the first turn waits up to 5s for initial MCP connects so it reflects real state, and changes are re-injected next turn.\n\nExtensions can be reloaded via `\u002Freload`; there is no automatic file watching. Reloading extensions rebuilds the system prompt, which might impact prompt cacheing.\n\nExtensions can also register top-level CLI verbs via the `cli_verbs:` frontmatter key — `fir \u003Cverb> [args...]` spawns the extension cold (no session) and routes via a `cli_invoke` JSON-RPC request. The extension drives fir's TTY through `host.println()`\u002F`host.readline()` etc. Built-in subcommands cannot be shadowed; verb collisions are a startup error. `fir observe` and `fir send` are the first users; see `docs\u002Fdesign\u002Fextension-cli-verbs.md` and `docs\u002Fextension-protocol.md` § CLI Verbs.\n\n## Skills\n\nSkills are Markdown instruction files at `.fir\u002Fskills\u002F\u003Cname>\u002FSKILL.md` with YAML frontmatter. They provide specialized workflows the agent can follow. Skills are auto-discovered from project, user, and builtin directories.\n\nUse `\u002Fskills` to list loaded skills, `\u002Freload` to pick up changes. Use `fir skills` to list all skills, `fir skills show \u003Cname>` (or shorthand `fir skills \u003Cname>`) to inspect one — flags `--full` to dump the SKILL.md body, `--path` to print only the file path — and `fir skills install \u003Cname>` to extract a builtin skill for customisation.\n\nSkills can also be invoked directly from the CLI: `fir \u002F\u003Cskill-name> [task...]` rewrites into an initial agent message that points at the named skill and supplies the rest of the positional arguments as the task body. Composes with all flags (`-p`, `--model`, `-c`, …). Bash and zsh completion enumerate `\u002F\u003Cskill>` candidates dynamically from `fir skills list`.\n\n`fir \u002F\u003Cname>` also runs **slash commands**: if no skill matches, the name is resolved against the built-in slash commands (`\u002Fchangelog`, `\u002Fsession`, `\u002Fmcp`, `\u002Fmodel X`, `\u002Ftheme`, `\u002Fskills`, `\u002Fplan`, …) and extension-provided commands, and dispatched exactly as if typed at the interactive prompt. Resolution order is skill first (back-compat), then command. Builtin slash commands are declared once in `pkg\u002Fslashcmd` and shared by the TUI and ACP (each marked mode-only where it is, e.g. TUI selectors like `\u002Ftheme`, ACP's `\u002Fcontinue`); `\u002Fhelp` lists the ones available in the current mode. `fir \u002F\u003Cname>` is TUI-bound, so in headless modes (`-p`, `--output-mode json`, ACP) they fail fast with `command \u002Fx requires interactive mode` rather than being sent to the model.\n\n## External Packages\n\nInstall\u002Fmanage external packages (git repos or local paths) that contribute skills, extensions, and themes:\n\n```\nfir install github.com\u002Fuser\u002Ffir-pack        # install to user scope (\u003Cagent-dir>\u002Fpackages\u002F)\nfir install .\u002Flocal\u002Fpath --local            # install to project scope (.fir\u002Fpackages\u002F)\nfir uninstall github.com\u002Fuser\u002Ffir-pack      # remove a package\nfir packages list                           # list installed packages (source, scope, skill\u002Fext counts, path)\nfir packages update [source]                # pull latest for one or all packages\n```\n\nPackages are stored in `settings.json` under `\"packages\"`. Each entry is a string (`\"github.com\u002Fuser\u002Frepo\"`) or an object with `\"source\"` and optional per-type filters. Installed package skills, extensions, and themes are automatically loaded.\n\n**Agent Plugins.** A package whose root has `plugin.json` is loaded as an [Agent Plugin](https:\u002F\u002Fagent-plugins.org\u002Fspecification) (v1.0.0) instead: `plugin.json` is validated (invalid → the whole plugin contributes nothing), skills come only from `skills\u002F*\u002FSKILL.md` (invalid ones skipped), and `\u003Cplugin>\u002Fmcp.json` servers join MCP config as the lowest layer (user\u002Fproject entries of the same name win, with a warning). Stdio servers run with cwd = plugin root and get `PLUGIN_ROOT` and `PLUGIN_DATA` (`\u003Cagent-dir>\u002Fplugin-data\u002F\u003Cname>-\u003Csource-hash>\u002F`, or `.fir\u002Fplugin-data\u002F…` for `--local`; created only for plugins with stdio servers; kept across updates). `fir.json` and auto-discovery are ignored for plugins. Diagnostics print at `fir install` and in startup MCP warnings. fir reserves the client namespace `dev.fir` (no contents defined). User guide: `docs\u002Fagent-plugins.md`. Code: `pkg\u002Fagentplugin\u002F`.\n\n## Resource Lookup Paths\n\nSkills and themes are discovered from multiple locations. Each\nloaded resource is tagged with an **origin** that identifies where it came\nfrom:\n\n| Origin | Where |\n|--------|-------|\n| `builtin` | embedded in the binary |\n| `user` | `\u003Cagent-dir>\u002Fskills\u002F` (and equivalents for themes) |\n| `project` | `\u003Ccwd>\u002F.fir\u002Fskills\u002F` |\n| `pkg:\u003Csource>` | installed package, e.g. `pkg:github.com\u002Fkfet\u002Ffoo` |\n| `path:\u003Cbasename>` | path supplied via `--skill \u003Cpath>` or the `\"skills\"` settings array |\n\nOrigins are surfaced via the `\u003Cname>__\u003Cid>` form below; they are not\nhierarchical \"wins one, loses another\" priorities.\n\n### Coexistence and Disambiguation\n\n**Same-named skills coexist by default.** A user skill called `release` and a\npackage skill called `release` will both load. Each gets a unique ID of the\nform `\u003Csanitized-origin>__\u003Cname>`, e.g. `user__release` and\n`pkg_github_com_kfet_release_tools__release` (every character outside\n`[a-z0-9_]` in the origin is replaced with `_`, MCP-tool-name style).\n\nThe agent references skills by **bare name when unique**, and by **full ID\nwhen ambiguous**. The `\u003Cavailable_skills>` block in the system prompt\nautomatically switches to the disambiguated form when a name conflict\nexists, and notes this to the agent.\n\n`fir skills list` shows the ID column and tags rows with `[ambiguous]` or\n`[overrides …]`. `fir skills show \u003Cname-or-id>` accepts either form; if a\nbare name is ambiguous it prints the candidate IDs and exits non-zero.\nSlash invocations (`fir \u002F\u003Cname>`) follow the same rule.\n\n### Overriding Another Skill\n\nTo replace another same-named skill instead of coexisting with it, add\n`override:` to your skill's frontmatter:\n\n```yaml\n---\nname: release\ndescription: My customised release flow\noverride: true                  # replace any other skill named \"release\"\n---\n```\n\nTwo forms are supported:\n\n- **`override: true`** — your skill replaces every other skill with the same\n  bare name. Use this for the common case (\"I want my `release` to *be* the\n  release skill\"). When two skills both claim `override: true`, the\n  highest-precedence origin wins (user > project > path > pkg > builtin),\n  the loser stays loaded under its disambiguated ID, and a warning is\n  emitted.\n- **`override: \u003Cfull-id>`** — your skill replaces one specific target.\n  Useful when you want to coexist with some same-named variants but\n  shadow one in particular:\n\n  ```yaml\n  override: pkg_github_com_kfet_release_tools__release\n  ```\n\n  The origin portion may be written either as the raw form (`pkg:github.com\u002F…`)\n  or the sanitized form (`pkg_github_com_…`); both are accepted.\n\nDiagnostics emitted at load time:\n\n- `duplicate-name` (info) — same-named skills coexist; reference by ID.\n- `shadowed` (warning) — a skill was suppressed by an override.\n- `override-conflict` (warning) — multiple skills claimed `override: true`\n  for the same name; the highest-precedence one won.\n\nThe same scheme applies to extensions and themes (rolling out;\nskills are first).\n\n### Settings Path Resolution\n\nPaths in `\"skills\"`, `\"themes\"`, and `\"extensionPaths\"` settings arrays support three forms:\n\n| Form | Example | Resolves to |\n|------|---------|-------------|\n| Absolute | `\"\u002Fopt\u002Fshared\u002Fskills\"` | `\u002Fopt\u002Fshared\u002Fskills` |\n| Home-relative | `\"~\u002Fmy-skills\"` | `$HOME\u002Fmy-skills` |\n| Relative | `\"skills\"` | `$CWD\u002Fskills` (resolved at startup) |\n\n**Relative paths are resolved against the current working directory**, not against the settings file. This is intentional — it makes relative paths portable across projects:\n\n```jsonc\n\u002F\u002F In ~\u002F.config\u002Ffir\u002Fsettings.json (global):\n{\n  \"skills\": [\"skills\"],       \u002F\u002F → finds .\u002Fskills\u002F in any project that has one\n  \"extensionPaths\": [\"extensions\"]  \u002F\u002F → finds .\u002Fextensions\u002F in any project\n}\n```\n\nThis lets you establish a convention (e.g. \"every project keeps skills in `.\u002Fskills\u002F`\") and have fir discover them automatically without per-project configuration. Paths that don't exist are silently skipped.\n\nFor project-specific settings (`.fir\u002Fsettings.json`), relative paths also resolve against cwd (the project root), which is the natural expectation.\n\n## Background jobs in the `bash` tool\n\nBackground jobs started by the `bash` tool (`cmd &`, `nohup cmd &`, `(sleep N; cmd) &`) keep running after the call returns. Output goes to an in-memory pipe, not to files. The result gives a job id. `bash job=\u003Cid>` returns the job's status and the output since the last read, from a 1MB ring buffer; `job=\u003Cid>, kill=true` stops it; `jobs=true` lists jobs. An exited job is forgotten once it has been read. Timeout or abort kills the whole group. At most 10 background jobs run at once; a call that would start an 11th fails with an error listing the running jobs. Surviving jobs are killed (SIGTERM, then SIGKILL) when the fir process exits or before `\u002Freexec`, and their pipes are closed. If fir crashes, the kernel closes the read ends and the jobs die of SIGPIPE on their next write. Jobs do not outlive the session, so for a delayed action wait inside the session, or use `watcher_add`.\n\n## Remote hosts (`remote` extension)\n\nThe builtin `remote` extension gives the agent six tools for working over ssh, so remote work never needs hand-written ssh one-liners inside `Bash`:\n\n- **`rexec(host, command, timeout_s, cwd, detach)`** — run a shell script on a remote host. The script is shipped on **ssh stdin** to `bash -l -s`, never as `ssh host \"bash -lc '...'\"`, so nothing is re-split by an intermediate shell and no escaping is needed. The `timeout_s` bound is a one-line bash supervisor passed as the ssh command — it backgrounds that `bash -l -s` under job control (`set -m`), so the command leads its own process group (TERM then KILL on expiry, exit 124) while still reading your script straight off the ssh channel. **Not** GNU `timeout`, which does not exist on macOS. `detach=True` runs it under `systemd-run --user --collect` (fallbacks: double-forked `setsid`, then `set -m` + `nohup` for macOS) on the remote box and returns a job id.\n- **`rjob(host, id, action)`** — `status` \u002F `log` \u002F `tail` \u002F `kill` for a detached job. State lives entirely in `~\u002F.cache\u002Ffir\u002Frjobs\u002F\u003Cid>.{log,rc,pid}` **on the remote host** — there is no local registry to drift.\n- **`rput` \u002F `rget`** — scp file transfer sharing the same flags and result envelope.\n- **`rtmux(host, action, ...)`** — `ls | new | send | cap | kill`. tmux on the remote host **is** the PTY; every action is a stateless short exec (no held `ssh -tt`). `cap` returns `unchanged: true` when the pane is byte-identical to the last capture, so polling is cheap.\n- **`rhosts(probe)`** — list `Host` stanzas from `~\u002F.ssh\u002Fconfig` (Includes followed), optionally with a parallel reachability sweep.\n\nAll connection flags (`BatchMode`, `ConnectTimeout`, `ServerAliveInterval`, and a `ControlMaster` mux at `~\u002F.ssh\u002Ffir-cm-%C` with `ControlPersist=120`) are owned by the extension. Hosts come from `~\u002F.ssh\u002Fconfig`; there is no fir-side host registry. Every tool returns a discriminated envelope — `outcome` is one of `ok | nonzero_exit | timeout | unreachable | auth_failed | no_tmux | no_target`, with `stdout_bytes` always explicit. Only transport\u002Ftarget failures are tool errors; a remote command exiting nonzero is ordinary signal.\n\n**Self-host detection.** A fleet agent frequently asks for the box it is already running on by alias, and ssh loopback fails with `Permission denied (publickey)` because a host's key is not in its own `authorized_keys`. `rexec` \u002F `rjob` \u002F `rput` \u002F `rget` detect this and run **locally** instead, under an sshd-shaped scrubbed environment (`$HOME` cwd, bare PATH, no `TMUX`\u002F`FIR_*`\u002F`SSH_AUTH_SOCK`, same login shell, same timeout supervisor, plus the `XDG_RUNTIME_DIR` that `pam_systemd` would set so `detach=True` still gets a `systemd-run --user` unit). The envelope then carries `local: true`, and `rhosts` marks the entry `self: true`. The test is not a hostname comparison: `ssh -G` resolves the target, a `ProxyCommand`\u002F`ProxyJump`, a non-22 port, a different target user or an `ip_nonlocal_bind` kernel all bail straight back to ssh, and otherwise each resolved address is `bind()`-tested — an address binds only if it is configured on a local interface. Pass `via_ssh=True` (or set `FIR_REMOTE_FORCE_SSH=1`) when the ssh hop itself is what you are testing. `rtmux` never runs locally: it refuses a self-host outright, because `send-keys` into the local tmux server can land in the agent's own pane — use the `tmux-driver` skill there.\n\nThese tools make teleoperation cheap, which is a trap: for substantial work on another host, `rput` a brief and `rtmux new` an agent over there rather than driving the job one call at a time.\n\n## Key Concepts\n\n- **Sessions** — conversations are persisted and can be continued (`-c`) or resumed (`-r`). Sessions form a tree; double-Escape or `\u002Ftree` navigates branches. `\u002Fsession` (or `Ctrl+S`) toggles a sticky, collapsible session-info overlay above the editor (version, IDs, message\u002Ftoken stats, enabled extensions); toggle it again, or `Ctrl+L`, to dismiss it without leaving anything in the transcript. Use `\u002Fnew [prompt]` to start a fresh session, optionally with an initial prompt that is submitted immediately. `\u002Fnew` (and `Ctrl+N`) drop any queued messages and say so (\"Discarded N queued messages\"); a `self_handoff` instead carries them into the new session, delivered after the handoff prompt — preceded by any user message the old session had not yet answered (\"N unanswered messages carried into the new session\"). **Resuming a file (non-interactive):** `fir --resume \u003Cfile.jsonl> -p '...'` appends the turn to that file (a value is taken without `--at` only when it is an existing file or ends in `.jsonl`). **Forking at an entry (non-interactive):** `fir --resume \u003Csession> --at \u003Centry-id>` (or `--session \u003Cpath> --at \u003Centry-id>`) is `\u002Fresume` + `\u002Ftree` to that entry in a new process: it writes a child session (header `parentSession` points at the source; recorded invocation is inherited) holding the root→entry path and continues there; the parent file is never written. `\u003Csession>` is a path, a file name, or a session id fragment. An entry that leaves an assistant tool call without its result is rejected. The child's history is byte-identical to the parent's prefix, so on the same model\u002Ftools the Anthropic prompt cache hits. A running agent learns its own `session.file`, `session.leafId` and `session.forkAt` (the newest entry `--at` accepts — the leaf, unless the leaf is mid tool call) from `agent_introspect`. ACP: `session\u002Ffork` (capability `sessionCapabilities.fork`) forks at the parent's forkable leaf, or at `_meta.at: \"\u003Centry-id>\"` (advertised as `fork._meta.at: true`); the response's `sessionId` names the child. Each `session\u002Fprompt` response carries `_meta.leafId` (the turn's leaf; advertised as `promptCapabilities._meta.leafId`, omitted if mid tool call) — pass it as `_meta.at` to fork exactly after that turn. Legacy fir-only: `\"at\"` on `session\u002Fload`\u002F`session\u002Fresume` (spec route: `_meta.at`, child in `_meta.sessionId`). ACP `\u002Fresume \u003Cn|path> --at \u003Cid>` does the same in-session.\n  **Subagent with context: fork yourself.** Default way to start a subagent that needs this session's context (instead of a fresh session or an `aside` delegate that re-explores):\n  1. `agent_introspect` → `session.file`, `session.forkAt`.\n  2. In the SAME cwd on the SAME model: `fir --resume \u003Cfile> --at \u003CforkAt> --model \u003Csame> -p '\u003Ctask; return a short summary>'` (ACP: `session\u002Ffork`).\n  3. Use only the child's summary; the parent file is untouched.\n\n  Rules: same model and tool set or the cache misses (an `aside` delegate on a cheaper model always misses); fork promptly (5-min cache TTL); the child inherits the full context, so don't fork near the context limit; children that write files need their own worktree; prefer it when the parent holds >~30k tokens of useful context, use a fresh session for unrelated work. Measured 2026-10-03 on fir 1.25.0: forking a 97.7k-token session gave first-turn cacheRead 96,131 \u002F cacheWrite 711 (~98% cached); on 1.24.4 (before the prompt split) cacheRead was 0.\n- **Re-exec for local build testing** — use `\u002Freexec` to restart into the current binary while preserving the active session, or `\u002Freexec \u003Cpath>` to switch to a specific built binary.\n- **In-place update** — use `\u002Fupdate` to check for, download, and install the latest release, then automatically restart the session.\n- **Compaction** — when context grows large, fir automatically summarizes older messages to stay within the model's context window. Configurable via `settings.json`.\n- **Review & Fix** — use the `review-and-fix` skill to run a one-shot review pass over recent changes. Covers build breaks, security, correctness, test gaps, and simplification (code reuse, quality, efficiency). Fixes all issues found, verifies the build, and commits.\n- **Aside (ephemeral side queries)** — use `\u002Faside \u003Cquestion>` to ask a quick side question using the current session context without adding the exchange to history. The response is shown as a notification and the main conversation is unaffected. Works even while a task is streaming. When given tool calls, `\u002Faside` orchestrates multi-tool workflows ephemerally via `ctx.call_tool()` + `ctx.side_query()` — raw tool outputs never enter the main conversation, only the synthesis is returned. Use `\u002Faside read the 5 largest .go files and summarise` to ask the agent to build and run an aside automatically. The `aside` tool can also be called directly by the agent — including in **agentic delegate mode**: pass `goal` (with `delegate=true`, mutually exclusive with `tools`) and the cheap delegate model drives its own read-only calls (whichever of `read`\u002F`glob`\u002F`grep`\u002F`ls` this session has) in a loop until it can answer, bounded by `max_iterations` (default 8, clamped 1-20), a total tool-output cap and a wall clock; narrow the tool set with `allow_tools`. Agentic mode is delegate-only and never falls back to the executor model. Escalation (`escalate=true`) has a `mode`: `self` (advisor sees the full session, the default behaviour), `strong` (the model set as `\"strong_advisor\"` in `aside.json` — by default the strongest available advisor chain, Fable first — sees only a composed brief — original task, last few turns, the turns named in `refs` (ids or exact quotes), and the question — kept as one append-only, cache-friendly thread per session+model, compacted past ~40k tokens), or `auto` (default: strong when the user asks for it, when `high_stakes=true`, or when self was already consulted on the same problem; else self). Strong falls back to self with a note only when `strong_advisor` is `\"off\"` or no strong model is available. Every aside result (and `\u002Fadvise`) ends with a token-accounting footer — `[in 1.2k · read 48.3k · write 612 · out 900]` — summed over every LLM call the escalation made; `read` vs `write` shows whether it hit the advisor prompt cache. For a subagent that needs this session's context to *do* work, fork yourself (see Sessions) rather than an aside delegate, which re-explores and misses the cache. Advisor replies that are only tool calls (native or written as `\u003Cinvoke>` text) are rejected and retried, never returned; the default advisor chain ends with a non-adaptive model at effort `off` (add one yourself, e.g. `anthropic\u002Fclaude-sonnet-4-5:off`, to an explicit `aside.json` chain); every failed advisor attempt is logged to the doctor log as `advisor_failure` with a `kind`.\n- **Thinking levels** — control reasoning depth: none, minimal, low, medium, high. Toggle with `Shift+Tab` or `--thinking`.\n- **Tool steering** — `\"steeringMode\"` in settings controls whether the agent runs tools one-at-a-time or in parallel.\n- **call_tool bridge** — extensions can call any registered tool (built-in, extension, or MCP) programmatically via `ctx.call_tool(name, params)`. Results are returned directly and never enter conversation history. This enables extensions to build rich orchestration workflows.\n- **MCP channel servers** — MCP servers that advertise the `claude\u002Fchannel` experimental capability can push messages into the running session via `notifications\u002Fclaude\u002Fchannel` notifications. Messages are injected into the agent conversation automatically. The server's `channel_reply` tool is a regular MCP tool. Configure channel servers in `.fir\u002Fmcp.json` like any other MCP server — no special config needed.\n- **MCP configuration** — configure MCP servers in `.fir\u002Fmcp.json` (project) or `\u003Cagent-dir>\u002Fmcp.json` (global, e.g. `~\u002F.config\u002Ffir\u002Fmcp.json`). You can also drop per-server files into the global `~\u002F.config\u002Ffir\u002Fmcp.d\u002F*.json` directory — handy for managing one MCP per file. All sources are merged with precedence (low to high): (1) `~\u002F.config\u002Ffir\u002Fmcp.json`, (2) `~\u002F.config\u002Ffir\u002Fmcp.d\u002F*.json` (lexically sorted), (3) `\u003Cproject>\u002F.fir\u002Fmcp.json`.\n\n  **Field reference — read it, do not recall it.** The complete, authoritative definition of every `mcpServers` entry and every `auth` key lives in `mcp-config-contract.md`, next to this file in the skill directory. It is fir's own source, embedded verbatim, so it can never drift from the running binary. When a user asks what a field does, which fields exist, what a default is, or whether some key is still supported, **read that file** and answer from it. Do not answer such questions from memory of this skill, and do not reproduce the field list here. What follows is the operational half: worked examples, the login flow, and how to choose.\n\n  Three transports are supported:\n  - **stdio** (default) — launches a local subprocess:\n    ```json\n    {\n      \"mcpServers\": {\n        \"filesystem\": {\n          \"command\": \"npx\",\n          \"args\": [\"-y\", \"@modelcontextprotocol\u002Fserver-filesystem\", \"\u002Fhome\u002Fuser\u002Fprojects\"],\n          \"env\": { \"NODE_PATH\": \"\u002Fusr\u002Flocal\u002Flib\u002Fnode_modules\" }\n        }\n      }\n    }\n    ```\n  - **streamable** (recommended for remote) — connects to a remote HTTP MCP server:\n    ```json\n    {\n      \"mcpServers\": {\n        \"my-remote\": {\n          \"transport\": \"streamable\",\n          \"url\": \"https:\u002F\u002Fmy-server.example.com\u002Fmcp\"\n        }\n      }\n    }\n    ```\n  - **sse** — connects via Server-Sent Events (legacy HTTP transport):\n    ```json\n    {\n      \"mcpServers\": {\n        \"legacy\": {\n          \"transport\": \"sse\",\n          \"url\": \"https:\u002F\u002Fold-server.example.com\u002Fsse\"\n        }\n      }\n    }\n    ```\n  - Any server may also declare `roots` — a list of `file:\u002F\u002F` URIs that fir advertises to the server as its filesystem roots, for servers that scope their operations to them:\n    ```json\n    {\n      \"mcpServers\": {\n        \"filesystem\": {\n          \"command\": \"npx\",\n          \"args\": [\"-y\", \"@modelcontextprotocol\u002Fserver-filesystem\"],\n          \"roots\": [\"file:\u002F\u002F\u002Fhome\u002Fuser\u002Fprojects\", \"file:\u002F\u002F\u002Fsrv\u002Fdata\"]\n        }\n      }\n    }\n    ```\n    When omitted (the default) fir advertises a single root: the process working directory. Entries are sent verbatim, so they must be `file:\u002F\u002F` URIs, not bare paths.\n- **MCP files extension (experimental)** — per-server opt-in `\"files\": {\"enabled\": true}` turns on the draft `io.modelcontextprotocol\u002Ffiles` extension, used only with servers that advertise it on protocol 2026-07-28+. Tool arguments marked `x-mcp-file` are shown to the model as `{\"path\": \"\u003Clocal file>\"}`; fir checks `accept`\u002F`maxSize`, sends the file inline (within the server's `maxInlineBytes`) or uploads it to the server's `upload_required` target and retries the call, so neither bytes nor target URLs enter context. Download targets in a result's `_meta` are fetched (size\u002Fdigest checked, 100 MiB default cap via `files.maxDownloadBytes`) into a private temp dir and the saved path is appended to the result. Sending a file needs approval: ACP asks the client with `session\u002Frequest_permission`; TUI and headless sessions have no prompt and refuse unless `files.autoApprove` is set. Targets: https only, no redirects, MCP credential only on `credentials:\"include\"` same-origin\u002F`path` targets, no private addresses; `files.allowedOrigins` restricts targets and is the only way to allow loopback\u002Fprivate or plain-http loopback targets. Design notes: `docs\u002Fmcp.md` (Files extension). Code: `pkg\u002Fmcp\u002Ffiles*.go`.\n- **MCP authentication (OAuth)** — remote (`streamable`\u002F`sse`) MCP servers need **no configuration** to authenticate. fir connects unauthenticated; on a `401` it runs the MCP authorization-spec discovery chain automatically — RFC 9728 protected-resource metadata (from the `WWW-Authenticate` challenge, falling back to the well-known path), RFC 8414 authorization-server metadata, RFC 7591 dynamic client registration — then an authorization-code + PKCE login with the RFC 8707 `resource` indicator. Tokens are stored in `auth.json` under an `mcp:\u003Cserver>` key, bound to the server URL they were minted for, refreshed proactively before expiry and silently on a 401.\n  - fir never opens a browser on its own — the MCP manager holds no login-UI hook at all, so no reconnect cycle or background goroutine can start one. A server that needs a login reports `run: \u002Fmcp login \u003Cserver> (or: fir mcp login \u003Cserver>)`. Log in with **`fir mcp login \u003Cserver>`** from a terminal, or **`\u002Fmcp login \u003Cserver>`** inside a session (TUI or ACP — over ACP a typed `\u002Fmcp login` only completes if the browser is on the agent host; ACP clients\u002Frelays use `authenticate` methodId `mcp:\u003Cserver>` with `_meta.auth.{interactive,sessionId}`, a two-call URL-out\u002Fpaste-back flow documented in `docs\u002Facp-interactive-auth.md`); **`fir mcp logout \u003Cserver>`** \u002F **`\u002Fmcp logout \u003Cserver>`** removes stored credentials; **`fir mcp list`** shows each server's auth state. After logging in out-of-band, **`\u002Fmcp reload [\u003Cserver>]`** re-reads configs and credentials and reconnects servers that are down, so their tools load without a new session. Per-server health (`ok | connecting | needs_auth | token_expired | error | disabled`, config source, last error, tool count, fix) is in `agent_introspect` under `mcp.servers` and pushed to extensions as the `mcp_state` event. Set `\"disabled\": true` on a server to keep it configured but never connect.\n  - Credentials never ride plaintext: a non-`https:\u002F\u002F` server URL is rejected unless the host is loopback (`127.0.0.1`, `::1`, `localhost`). Use `\"auth\": {\"mode\": \"none\"}` for a plain-HTTP server that needs no credentials.\n  - Optional `auth` overrides (all absent by default — a spec-compliant server needs none):\n    ```json\n    {\n      \"mcpServers\": {\n        \"pat-server\":  {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fa.example\u002Fmcp\",\n                        \"auth\": {\"token\": \"${MY_SERVER_PAT}\"}},\n        \"no-dcr\":      {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fb.example\u002Fmcp\",\n                        \"auth\": {\"client_id\": \"abc123\", \"scopes\": [\"mcp:read\"]}},\n        \"no-auth\":     {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fc.example\u002Fmcp\",\n                        \"auth\": {\"mode\": \"none\"}},\n        \"bad-metadata\":{\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fd.example\u002Fmcp\",\n                        \"auth\": {\"authorization_servers\": [\"https:\u002F\u002Flogin.d.example\"],\n                                 \"scopes\": [\"mcp:read\"]}},\n        \"corp\":        {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fmcp.corp.internal\u002Fmcp\",\n                        \"auth\": {\"allow_private_network\": true}},\n        \"okta\":        {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fe.example\u002Fmcp\",\n                        \"auth\": {\"client_id\": \"0oa1b2c3d4EXAMPLE\",\n                                 \"redirect_uri\": \"http:\u002F\u002Flocalhost:8765\u002Foauth\u002Fcallback\"}}\n      }\n    }\n    ```\n    **Choosing a mode** (the field reference has the exact semantics; this is when to reach for each):\n    - *Leave `auth` out entirely* — the right answer for almost every server. fir discovers and logs in on its own.\n    - *Add `token`* when the server issues personal access tokens and has no OAuth at all. Bearer mode is inferred; always use `${VAR}` rather than pasting a secret into the file.\n    - *Add `client_id` (+ `scopes`)* when login fails at the **registration** step — the authorization server does not do RFC 7591 dynamic registration.\n    - *Add `authorization_servers`* when login fails at the **discovery** step — the server publishes no RFC 9728 metadata, or publishes wrong metadata.\n    - *Add `redirect_uri`* when login reaches the authorization server but it rejects the callback — the issuer demands an exactly pre-registered redirect and ignores RFC 8252 §7.3 port variance (Okta is the canonical case). Pair it with `client_id`.\n    - *Add `allow_private_network`* when discovery cannot even connect because the issuer is on an internal network behind a private DNS name.\n    - *Set `mode: \"none\"`* only to opt out deliberately — e.g. a plain-HTTP local server that needs no credentials.\n\n    **Common failure modes:**\n    - `run: \u002Fmcp login \u003Cserver>` — expected on first use. fir never opens a browser by itself; the user must run `fir mcp login \u003Cserver>` or `\u002Fmcp login \u003Cserver>`.\n    - Credentials refused before any request — the server URL is not `https:\u002F\u002F` and not loopback. fir will not send a token in cleartext; fix the URL, or use `mode: \"none\"` if there is genuinely nothing to protect.\n    - A pinned `redirect_uri` that \"looks identical\" but fails — it is compared literally, so `localhost` and `127.0.0.1` are different values. Match what is registered character for character, and make sure the pinned port is free at login time.\n    - Browser cannot reach the box's loopback (phone sign-in through a gateway)? Pin an `https` `redirect_uri` plus `callback_listen: \"127.0.0.1:\u003Cport>\"`, or set `FIR_MCP_OAUTH_REDIRECT_BASE=\u003Chttps base>` so fir uses `\u003Cbase>\u002F\u003CP>\u002Fcallback` while listening on `127.0.0.1:\u003CP>`. The proxy must forward the path unchanged. The variable is ignored when `client_id` or `redirect_uri` is set.\n    - `allow_private_network` set but still blocked — a private IP *literal* is rejected upstream with no opt-out. Use a DNS name with a real certificate.\n    - OAuth-only keys in `bearer`\u002F`none` mode are rejected loudly rather than ignored, because the OAuth chain never runs there; remove them or switch mode.\n    - Stale credentials after changing `authorization_servers` are invalidated automatically; tokens live in `auth.json` under `mcp:\u003Cserver>`, and `fir mcp logout \u003Cserver>` clears them.\n\n- **MCP inspection** — use `\u002Fmcp` to see all configured MCP servers with their connection status, transport, capabilities (resources, prompts), and a full list of exposed tools with descriptions. Use `\u002Fmcp \u003Cname>` for detailed info about a specific server. Use `\u002Fmcp reload` to re-read `mcp.json` and `mcp.d\u002F*.json` configs from disk and apply the diff to running servers without a full session reload; it reports collisions when multiple files define the same server name.\n\n## settings.json Reference\n\nAll fields are optional. Nested objects merge recursively; arrays and primitives from overrides win.\n\n```jsonc\n{\n  \u002F\u002F Model defaults\n  \"defaultProvider\": \"anthropic\",        \u002F\u002F Provider name\n  \"defaultModel\": \"claude-sonnet-4-20250514\",  \u002F\u002F Model ID\n                                         \u002F\u002F A pin here beats the provider's own default forever.\n                                         \u002F\u002F \u002Fdoctor warns when the provider has moved on to a\n                                         \u002F\u002F newer generation of the same model line (never rewrites it).\n  \"defaultThinkingLevel\": \"medium\",      \u002F\u002F none, minimal, low, medium, high\n\n  \u002F\u002F UI\n  \"theme\": \"dark\",                       \u002F\u002F Theme name or path\n  \"hideThinkingBlock\": false,            \u002F\u002F Hide thinking blocks in output\n  \"collapseChangelog\": false,            \u002F\u002F Collapse changelog on startup\n  \"turnCache1h\": true,                   \u002F\u002F Anthropic: cache each turn's user message for 1h (TUI\u002FACP; env FIR_TURN_CACHE_1H=0\u002F1 overrides)\n  \"doubleEscapeAction\": \"tree\",          \u002F\u002F \"tree\" or \"quit\"\n  \"autocompleteMaxVisible\": 5,           \u002F\u002F Max autocomplete suggestions shown\n  \"showHardwareCursor\": false,           \u002F\u002F Show hardware cursor\n\n  \u002F\u002F Shell\n  \"shellPath\": \"\u002Fbin\u002Fbash\",              \u002F\u002F Shell for bash tool\n  \"shellCommandPrefix\": \"\",              \u002F\u002F Prepended to every bash command\n\n  \u002F\u002F Compaction\n  \"compaction\": {\n    \"enabled\": true,                     \u002F\u002F Auto-compact when context is large\n    \"reserveTokens\": 16384,              \u002F\u002F Tokens reserved for compaction summary\n    \"keepRecentTokens\": 20000,           \u002F\u002F Recent tokens to keep uncompacted\n    \"maxContextTokens\": 0               \u002F\u002F Hard token cap — compact when exceeded (0 = disabled)\n  },\n\n  \u002F\u002F Branch summary\n  \"branchSummary\": {\n    \"reserveTokens\": 16384               \u002F\u002F Tokens reserved for branch summaries\n  },\n\n  \u002F\u002F Retry on transient errors\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"maxDelayMs\": 60000\n  },\n\n  \u002F\u002F Thinking token budgets per level\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  },\n\n  \u002F\u002F Terminal display\n  \"terminal\": {\n    \"showImages\": true,                  \u002F\u002F Render images in terminal\n    \"clearOnShrink\": false               \u002F\u002F Clear screen when terminal shrinks\n  },\n\n  \u002F\u002F Image handling\n  \"images\": {\n    \"autoResize\": true,                  \u002F\u002F Auto-resize large images\n    \"blockImages\": false                 \u002F\u002F Block all image content\n  },\n\n  \u002F\u002F Markdown rendering\n  \"markdown\": {\n    \"codeBlockIndent\": \"  \"              \u002F\u002F Indentation for code blocks\n  },\n\n  \u002F\u002F Model filtering (glob patterns; empty = all)\n  \"enabledModels\": [\"claude-*\", \"gpt-4*\"],\n\n  \u002F\u002F Extension\u002Fskill\u002Ftheme allowlists (empty = all)\n  \"extensions\": [],\n\n  \u002F\u002F Extension search directories (relative paths resolve against cwd)\n  \u002F\u002F   \"extensionPaths\": [\"extensions\"]  → looks for .\u002Fextensions\u002F in every project\n  \"extensionPaths\": [],\n\n  \u002F\u002F Additional resource lookup paths (resolved per-invocation relative to cwd)\n  \u002F\u002F Use absolute paths, ~\u002F... paths, or relative paths.\n  \u002F\u002F Relative paths resolve against the working directory, making them portable:\n  \u002F\u002F   \"skills\": [\"skills\"]  → looks for .\u002Fskills\u002F in every project you open\n  \u002F\u002F   \"skills\": [\"~\u002Fshared-skills\"]  → always points to the same directory\n  \"skills\": [],\n  \"themes\": [],\n  \"enableSkillCommands\": true,           \u002F\u002F Allow \u002Fskills slash commands\n  \"enableSysExtensions\": true,           \u002F\u002F Allow extensions to prepend [SYS_EXT] context blocks\n\n  \u002F\u002F Agent behavior\n  \"steeringMode\": \"one-at-a-time\",      \u002F\u002F \"one-at-a-time\" or \"auto\"\n  \"followUpMode\": \"one-at-a-time\",      \u002F\u002F \"one-at-a-time\" or \"auto\"\n  \"transport\": \"sse\",                    \u002F\u002F Transport protocol\n  \"serverTools\": [\"web_search\"],         \u002F\u002F Anthropic server-side tools: \"web_search\", \"web_fetch\", \"code_execution\"\n                                          \u002F\u002F You can also use raw types (e.g. \"web_search_20260209\"); model support is controlled per-model in models.json\n  \"serverCompaction\": {                  \u002F\u002F Anthropic server-side context compaction\n    \"enabled\": true,\n    \"triggerTokens\": 150000,             \u002F\u002F When to trigger (min 50000, default 150000)\n    \"instructions\": \"\"                   \u002F\u002F Custom summarization prompt (replaces default)\n  },\n\n  \"debugLog\": {                          \u002F\u002F Debug log (debug.log) rotation\n    \"maxSizeMB\": 100,                    \u002F\u002F Rotate when the file reaches this size\n    \"keep\": 20,                          \u002F\u002F Number of compressed backups to retain\n    \"compress\": true,                    \u002F\u002F gzip backups (debug.log.N.gz)\n    \"checkEveryWrites\": 1000,            \u002F\u002F Stat the file at most once per N writes\n    \"checkEverySeconds\": 30              \u002F\u002F ...or once per this many seconds\n  },\n\n  \"sessionStores\": [                     \u002F\u002F Extra READ-ONLY session stores (global settings only;\n    { \"path\": \"~\u002Ffir-traces\u002Fzbox\",       \u002F\u002F   see \"Session stores\"). env FIR_SESSION_STORES replaces it.\n      \"label\": \"zbox\" }                  \u002F\u002F   label defaults to the dir name; \"local\" is reserved\n  ],\n\n  \"auth\": {                              \u002F\u002F Credential storage (global settings only; see \"Secret storage\")\n    \"storage\": \"file\"                    \u002F\u002F file (default) | auto (OS secret store if it answers a probe,\n                                         \u002F\u002F else auth.json) | keyring (require it). env FIR_AUTH_STORAGE overrides.\n  },\n\n  \"mcp\": {                               \u002F\u002F MCP (Model Context Protocol) behaviour\n    \"toolTimeoutSeconds\": 120            \u002F\u002F Default per-call timeout for model-dispatched MCP tools\n                                         \u002F\u002F (default 120; \u003C=0 disables; env FIR_MCP_TOOL_TIMEOUT overrides).\n                                         \u002F\u002F Extension\u002Fpipe\u002Fwait calls (incl. timeout=-1) are never clipped.\n  },\n\n  \u002F\u002F Internal (managed by fir)\n  \"lastChangelogVersion\": \"\",            \u002F\u002F Last changelog version shown to user\n  \"packages\": []                         \u002F\u002F Package metadata\n}\n```\n","# fir — AI Coding Agent\n\nYou are fir. fir is a fast, portable AI coding agent. Single Go binary, no runtime dependencies. \u003C2>0 MB static binary.\n\nFor detailed CLI flags, run `fir --help`. For interactive commands and keyboard shortcuts, use `\u002Fhelp` inside fir — it toggles a sticky, collapsible overlay above the editor (run `\u002Fhelp` again, or `Ctrl+L`, to dismiss it).\n\n### Logging & verbosity\n\n- `-v` enables Debug logging to `\u003Cagent-dir>\u002Fdebug.log` (same as `--debug`; `\u003Cagent-dir>` defaults to `~\u002F.config\u002Ffir`).\n- `-vv` enables Trace logging — per micro-op events such as cache-prefix invalidations and MCP per-event chatter. `-vvv` and beyond clamp to `-vv` with a warning.\n- `-V` (or `--version`) prints the version. The old `-v` short alias for `--version` is gone; lone `fir -v` prints a migration note to stderr.\n- `FIR_LOG_LEVEL=info|debug|trace` (or a raw slog numeric level) sets the level via env; wins over `FIR_DEBUG=1`. CLI `-v`\u002F`-vv` win over both.\n- `debug.log` is rotated in place when it grows past a size cap. Tune via `settings.json` `\"debugLog\"` (global or project): `maxSizeMB` (default 100), `keep` (default 20 compressed backups), `compress` (default true), `checkEveryWrites` (default 1000), `checkEverySeconds` (default 30). Backups are standard gzip (`debug.log.1.gz`, `.2.gz`, …) readable by `gunzip`\u002F`zcat`. Rotation is cross-process safe via an advisory flock on `debug.log.lock`.\n\n## Installing fir\n\n- **Homebrew** (macOS, Linux): `brew install kfet\u002Fai\u002Ffir` (auto-taps `kfet\u002Fhomebrew-ai`; the formula is regenerated by GoReleaser on each release and pulls binaries from the `kfet\u002Ffir-dist` mirror, so no GitHub auth is required). To pin or roll back to a specific minor channel: `brew install kfet\u002Fai\u002Ffir@0.29` — pinned and rolling formulas both provide `bin\u002Ffir`, so switching between them uses `brew unlink fir` \u002F `brew link --overwrite fir@0.29` (the rollback flow). The tap keeps the 10 most-recent minors.\n- **Install script** (macOS, Linux, Raspberry Pi): `curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002Fkfet\u002Ffir-dist\u002Fmain\u002Finstall.sh | sh`\n- **Self-update**: `fir update` (pulls from `kfet\u002Ffir-dist`).\n- **Check only**: `fir update -check` reports the running and latest versions and exits **3** when an update is available (0 when up to date, 1 on error), so a fleet script can branch on the status. It never downloads or replaces anything. Resolution goes through `github.com\u002Fkfet\u002Fdistkit`, the family's distribution library, which is wired in DORMANT: it resolves and reports, while fir's own stdlib updater (`pkg\u002Fupdate\u002Frelease.go`) performs the actual swap — it checks the download against the release's `checksums.txt` (catching corrupt or truncated downloads) and refuses to install on a mismatch or missing entry. The cached 24h background check runs both resolvers and records any disagreement in `\u003CagentDir>\u002Fupdate-check.json` (`distkit_version`, `distkit_error`), logging it at WARN; `fir update -check` prints the last disagreement if there was one.\n\n### Shell completion\n\nBash and zsh completion ship with fir. Homebrew and the install script drop them automatically; for manual setup or `go install` builds:\n\n```sh\nfir completion bash > ~\u002F.local\u002Fshare\u002Fbash-completion\u002Fcompletions\u002Ffir\nfir completion zsh  > \"${fpath[1]}\u002F_fir\" && compinit\n```\n\nThe completion covers every flag and subcommand and dynamically completes `--provider`, `--model`, `--extension`, `--skill`, `--session`, and installed packages by shelling out to `fir --list-models`, `fir extensions list`, etc. A build-time test (`cmd\u002Ffir\u002Fcompletion_test.go`) parses `args.go` and fails CI if any new flag\u002Fsubcommand is missing from the completion scripts.\n\nNot to be confused with `fir install \u003Csource>`, which installs *packages* (skills\u002Fextensions\u002Fthemes) into an existing fir install — see \"External Packages\" below.\n\n## Modes\n\n- **Interactive** (default) — full TUI with markdown rendering, streaming, slash commands, and live plan visualization (`\u002Fplan` to toggle)\n- **Print** (`-p` \u002F `--print`) — non-interactive one-shot; process prompt and exit\n- **ACP** (`--mode acp`) — Agent Client Protocol, JSON-RPC 2.0 over stdio for IDE integrations (e.g. Zed). Supports the unstable `session\u002Flist`, `session\u002Fresume`, `session\u002Ffork`, and `session\u002Frelease` methods. To curb unbounded session\u002Fsidecar growth when a relay never releases sessions, fir auto-reaps in-memory ACP sessions idle longer than `--acp-session-idle-ttl` (default 90m — keep above the 1h prompt-cache TTL; `0` disables) — an idle session then holds zero extension sidecars. Every session (all modes) saves its state — model, thinking, name, cwd, mode, `_meta`, session-scoped\u002Fclient `mcpServers` — next to its transcript (`\u003Csession>.jsonl.state.jsonl`, 0600) and restores it on `-c`\u002F`--session`\u002F`\u002Fresume` (see `docs\u002Fsession-restore.md`). In ACP the `sessionId` is a handle bound to the current transcript, so the next prompt on a reaped `sessionId` — even after a fir restart — rebuilds the session in place (same ID). `session\u002Fnew`\u002F`load`\u002F`resume` values override and are saved. A resume never waits for MCP servers: each is advertised from its saved tool schemas until it connects (calls to a down server return a tool error); a client replaces its servers with `session\u002Fload`. Unknown-session requests (prompt\u002Frelease\u002Fset_model\u002Fetc.) return the typed JSON-RPC error code `-32001` (session not found) so relays can transparently re-create the session. MCP data passthrough (capability `agentCapabilities._meta[\"dev.fir.mcp\u002Fv1\"]`): MCP tool calls carry `_meta[\"dev.fir.mcp\u002Fv1\"]` with the server, original tool name, tool definition `_meta` (e.g. `ui.resourceUri`), result `_meta` and `structuredContent`; MCP `resource`\u002F`resource_link` result blocks are kept as ACP resource blocks; extension methods `_fir\u002Fmcp\u002Fresources\u002Fread` and `_fir\u002Fmcp\u002Ftools\u002Fcall` reuse the session's MCP connections, limited to tools in the session's active tool set (see `docs\u002Facp-mcp-passthrough.md`). fir does no UI work — clients render MCP Apps themselves. Mid-turn steer (capability `_meta[\"dev.fir.steer\u002Fv1\"]`): `_fir\u002Fsession\u002Fsteer` `{sessionId, prompt}` queues content onto the running turn's steering queue (seen at the next tool boundary, turn not cancelled); with no running turn it returns `-32002` and queues nothing. `mode: \"record\"` with `{question, answer, forkSessionId?}` instead appends a `side_query` custom transcript entry that is never sent to the LLM, with or without a running turn (see `docs\u002Facp-steer.md`).\n\n## Configuration Hierarchy\n\nSettings merge in order (later wins):\n\n1. **Global config** — `\u003Cagent-dir>\u002Fsettings.json` (`~\u002F.config\u002Ffir` by default; override with `FIR_AGENT_DIR` or `fir --agent-dir \u003Cdir>`, with the CLI flag winning)\n2. **Project config** — `.fir\u002Fsettings.json` in project root\n3. **CLI flags** — override everything\n\nAll settings fields are optional. Project settings are merged on top of global settings field-by-field; nested objects merge recursively, arrays and primitives from the override win.\n\n### Global Directory (`\u003Cagent-dir>\u002F`, default `~\u002F.config\u002Ffir\u002F`)\n\n| File | Purpose |\n|------|---------|\n| `settings.json` | Default provider, model, thinking, compaction, retry, terminal, image settings |\n| `models.json` | Add or override models per provider (custom ids, base URLs, headers, costs, sweScore) |\n| `provider-usage.json` | Usage\u002Fauth warnings: `enabled`, `warn_levels` ([80,95]), `stale_after_seconds` (600), `refresh_grace_minutes` (15), `login_max_age_days` (0=off), `burst_points` (3), `burst_window_seconds` (600) — burst = account-wide 5h utilization jump read from response headers |\n| `keybindings.json` | Custom key bindings for interactive mode |\n| `sessions\u002F` | Saved conversation sessions |\n| `skills\u002F` | User-level skills (shared across projects) |\n| `extensions\u002F` | User-level extensions (shared across projects) |\n| `packages\u002F` | Installed external packages (user scope) |\n\n### Project Directory (`.fir\u002F` in project root)\n\n| Path | Purpose |\n|------|---------|\n| `settings.json` | Project-level setting overrides |\n| `keybindings.json` | Project-level keybinding overrides (merged on top of global) |\n| `skills\u002F` | Project-specific skills (auto-discovered) |\n| `extensions\u002F` | Project-specific extensions (auto-discovered) |\n| `packages\u002F` | Installed external packages (project scope, via `--local`) |\n| `mcp.json` | MCP server configuration (project-level) |\n\n### Project Context Files\n\nfir reads `AGENTS.md` (or `CLAUDE.md`) from the working directory and its ancestors, automatically including them in the system prompt. This is the primary way to give fir project-specific instructions.\n\n## Authentication\n\nTwo methods:\n\n1. **Environment variables** — set `\u003CPROVIDER>_API_KEY` (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). See the full list below.\n2. **OAuth** — use `\u002Flogin` in interactive mode, or `fir login \u003Cprovider-id>` from the shell. Supports Anthropic, Google Antigravity, OpenAI Codex, GitHub Copilot, and Poe. Credentials are persisted automatically.\n3. New OAuth providers can be added as extensions\n\n## Login subcommand\n\n`fir login \u003Cprovider-id>` runs the OAuth flow for a provider from the shell (no interactive TUI required) and writes credentials to `\u003Cagent-dir>\u002Fauth.json`. Because OAuth providers are contributed by auth extensions, `fir login` loads extensions before running the flow — honouring the `settings.json` `extensions` allowlist, `-e\u002F--extension`, `-d\u002F--disable-extension`, and `--no-extensions`, exactly like the main CLI. `fir login list` (or bare `fir login`) lists every provider registered after extensions load. Equivalent to the long-standing `--login \u003Cprovider-id>` flag; both paths share the same session-bootstrap code.\n\n### Multiple accounts per provider\n\nA provider can hold several accounts at once. Running `fir login \u003Cprovider-id>` a second time **adds** another account rather than evicting the first — e.g. a personal and a work Anthropic login coexist. Accounts are stored in `auth.json` under composite slot keys: the bare `\u003Cprovider>` key is the default account, and extra accounts use `\u003Cprovider>#\u003CaccountId>`. For Anthropic the account id is a **human-readable** slug built from the email and organization name (e.g. `anthropic#me@example.com-acme-corp`, falling back to uuids only when neither is available), and the selector label shows the display name (or email) plus the org — e.g. `Ada Lovelace (Acme Corp)`. The same user logged into two different orgs therefore yields two distinct, recognisable accounts. Older fir builds reading the file still find the default account and ignore the `#account` keys.\n\nEach account appears as its own labelled group in the model selector and `--list-models` (e.g. \"Anthropic (Claude Pro\u002FMax) (work@x.com)\"), so switching accounts live is just selecting a model under that group — no env juggling, no restart. `fir login list` also prints the stored accounts. Remove a single account with `fir logout \u003Cprovider[#account]>` (or `fir logout list` to see slots).\n\n### Keeping an OAuth login alive on an idle machine\n\n`fir auth refresh [provider-id | provider-id#account]` performs the OAuth refresh grant and nothing else — no model, no tools, no inference, no quota. It exists because fir otherwise only refreshes a token as a side effect of running a turn, so a box that sits idle (a bot host, a rarely-used fleet machine) eventually lets its *refresh* token expire and needs an interactive browser re-login. Put it in cron:\n\n```\n0 4 * * *  fir auth refresh\n```\n\n- Provider defaults to `anthropic`; any registered OAuth provider id works. Nothing provider-specific is hardcoded — endpoints, client id and expiry normalisation come from the extension's `declare_oauth_provider` spec and `auth_post_exchange` hook.\n- Every stored account slot of that provider is refreshed, default first. One slot failing does not stop the others, and the exit status is non-zero if any failed.\n- Pass an **account slot key** (`fir auth refresh anthropic#work@x.com`) to refresh just that one account. This is the same `provider#account` form this command prints, and the one `fir login list` and `fir logout` accept, so a slot key pasted straight back in works. An unknown slot under a valid provider lists the provider's stored slot keys instead of refreshing anything.\n- Output is one greppable tab-separated line per slot: `\u003Cslot> \u003Clabel> \u003Coutcome> expires=\u003CRFC3339> in=\u003Cduration>`, where outcome is `refreshed`, `fresh`, `skipped` or `failed`.\n- A credential more than an hour from expiry is left alone rather than needlessly rotated. `--within DURATION` moves that guard; `--force` rotates regardless.\n\n**Rotation is destructive — know this before you use `--force`.** OAuth refresh tokens rotate on every grant, and Anthropic **revokes the previous access token the instant the rotation happens** — it does not survive to its stated `expires`. fir handles this: any process resolving a key re-reads `auth.json` when it has changed on disk, and the request path re-reads unconditionally before reporting an auth failure, so a session that is live across a rotation picks up the new token and retries rather than dying on a `401 OAuth access token has been revoked`. Never hand-write `auth.json` or run a refresh grant outside fir — an out-of-tree writer racing fir over a rotating credential can replay a consumed refresh token and get the entire token family revoked.\n\n> **Copying the agent dir does NOT isolate a `--force` test.**\n> `cp -a ~\u002F.config\u002Ffir \u002Ftmp\u002Ffirtest && FIR_AGENT_DIR=\u002Ftmp\u002Ffirtest fir auth refresh --force`\n> looks safe and is not. The copy duplicates the *file*, but the refresh token inside it is\n> the same credential upstream — forcing a grant on the copy rotates the real credential at\n> the provider and revokes the access token every live fir session is holding. A copied\n> agent dir is only safe for paths that spend no grant (a plain `fir auth refresh` that\n> reports `fresh`, `skipped`, or an unknown-provider error). Genuinely isolated `--force`\n> testing needs a **separate login**, not a file copy.\n\n\n### Secret storage (`auth.storage`, `fir auth storage`)\n\nBy default every credential (provider logins, API keys, MCP OAuth tokens) is plain text in `auth.json` (0600, flock-guarded). Set `\"auth\": {\"storage\": \"auto\"}` (or `\"keyring\"`) in the **global** `settings.json` to move secrets into the OS secret store — macOS Keychain, Linux Secret Service (GNOME Keyring\u002FKWallet over D-Bus), Windows Credential Manager. `auth.json` then keeps only metadata (type, label, expiry) plus a `_fir_storage` marker; secrets are one keychain item per slot under service `fir`. `auto` falls back to `auth.json` when no store answers (e.g. headless Linux without a session bus); `keyring` errors instead. Once migrated the marker is authoritative: if the store later becomes unavailable fir reports an error rather than silently writing secrets back to the file.\n\n- `fir auth storage` — show where secrets live, the setting, and whether the OS store answers.\n- `fir auth storage keyring` — migrate now (each secret is written and read back before the file is stripped).\n- `fir auth storage file` — move secrets back into `auth.json` and delete the keychain items (set `auth.storage` to `file` first, or the next access migrates them again).\n\n### Amazon Bedrock accounts\n\n`fir login bedrock` configures one or more Bedrock accounts (these are credential configs, not OAuth flows). Three modes:\n\n- **iam-profile** — a named `~\u002F.aws` profile (SigV4). Profiles are seeded from `~\u002F.aws\u002Fconfig`.\n- **iam-keys** — explicit AWS access\u002Fsecret (and optional session) keys (SigV4).\n- **bearer** — a Bedrock API key \u002F bearer token (suppresses SigV4); seeded from `$AWS_BEARER_TOKEN_BEDROCK` if present.\n\nWith no flags it prompts interactively; non-interactive flags: `--mode`, `--account NAME`, `--region`, `--profile`, `--access-key`, `--secret-key`, `--session-token`, `--token`. Each account can carry a per-account `region` (which selects the regional `bedrock-runtime` endpoint) and per-model inference-profile ARN overrides (stored in the credential's `extra.modelOverrides`). Like OAuth accounts, multiple Bedrock accounts coexist and switch live via the selector; the credential type is stored as `aws_iam` (profile\u002Fkeys) or `api_key` (bearer) in `auth.json`.\n\n## Session observation (`fir observe` \u002F `fir send`)\n\nEvery fir session writes a JSONL transcript to disk via `SessionStore`. The builtin `observe` extension registers a sidecar at `$XDG_STATE_HOME\u002Ffir\u002Fagents\u002F\u003Csession-id>.json` containing discovery metadata (pid, socket path, transcript path, cwd, status, name) and listens on a Unix socket for input injection.\n\n`fir observe` and `fir send` are **CLI verbs of the `observe` extension** (declared via `cli_verbs:` in its frontmatter), dispatched cold by fir's verb-dispatcher. There is no Go subcommand for either — the entire implementation lives in `pkg\u002Fresources\u002Fbuiltin_extensions\u002Fobserve.py`. See `docs\u002Fdesign\u002Fextension-cli-verbs.md` for the mechanism.\n\nThe same extension also exposes:\n- **`\u002Fobserve`** and **`\u002Fsend`** slash commands — usable from inside an interactive session for snapshot inspection \u002F one-shot message injection.\n- **`observe_session`**, **`send_session`**, and **`stop_session`** AI tools — let the agent inspect, nudge, or terminate sibling sessions as part of a turn.\n\nSlash commands and tools take **snapshots** (last N transcript lines) and send single messages; they do not live-tail. Use `fir observe \u003Cid>` from another terminal for live observation. `observe_session` \u002F `\u002Fobserve` also accept a partial-range slice (`start`\u002F`end` tool params, or `--start=`\u002F`--end=` flags) to page through a long transcript by line number instead of pulling the tail.\n\nThe `observe` extension is a builtin and loads automatically in every session — no special flags needed. Observation commands run from any other terminal:\n\n```\nfir observe                        # list LIVE sessions (status idle\u002Frunning\u002Ferror\u002Fno-model\u002Fobserve-lost)\nfir observe --all                  # include ended + crashed sessions\nfir observe --json                 # the list as JSON (one status object per session)\nfir observe \u003Cid-prefix>            # status + last 50 entries, then exit (on a TTY: follow live)\nfir observe \u003Cid> -n 200            # snapshot of the last 200 entries; always exits\nfir observe \u003Cid> -f                # follow live until the session ends (Ctrl-C \u002F Ctrl-\\ to stop)\nfir observe \u003Cid> --status [--json] # status only: idle|running|error|no-model|observe-lost|ended|crashed,\n                                   #   model, current tool, run count, last error, startup notice\n                                   #   crashed = fir host dead; observe-lost = host alive but\n                                   #   not observable\u002Fsendable in real time (do not kill for it)\nfir observe \u003Cid> --wait [--timeout S]  # block until the current run ends (124 on timeout)\nfir observe \u003Cid-prefix> --json     # raw JSONL (snapshot; with -f a raw tail)\nfir observe --cwd .                # resolve by directory (symlinks resolved)\nfir observe \u003Cid> --interact        # follow + pipe stdin to the session\n\nfir send \u003Cid> 'message'            # send one message (args joined)\nfir send \u003Cid> --wait 'message'     # send, block until that turn ends, print the final reply\n                                   #   (exit 1 if the turn failed, 124 on --timeout S)\necho \"brief\" | fir send \u003Cid>       # piped stdin = ONE message (multi-line ok)\nfir send \u003Cid-prefix>               # on a TTY: interactive, Enter sends each line\nfir send \u003Cid> --steer|--follow ... # deliver as steer (interrupt) \u002F followUp (queue)\nfir send \u003Cid> --abort              # cancel the current turn\nfir send \u003Cid> --model prov\u002Fid ...  # switch provider+model (alone, or before the message)\n```\n\n`\u003Cid-prefix>` prefix-matches against session id, session name, or basename(cwd). First-line sigils in `fir send`: `!message` → steer, `+message` → followUp, `~` → abort, `\\!`\u002F`\\+`\u002F`\\~` to escape. `Ctrl-\\` detaches. In interactive `fir send` and `fir observe --interact`, a typed `\u002Fmodel provider\u002Fid` line switches the session's model instead of being sent. The `send_session` tool takes a `model` parameter for the same switch.\nA steer lands at the next turn boundary: most tools (bash) finish first, but `wait` yields within ~1s (`outcome=interrupted` + resume handle) and `pipe` stops between steps.\n\nStatus comes from the core-owned **`session\u002Fstatus` observable card** (`\u003Ctranscript>.cards`), which `AgentSession` writes in agent-loop order on every lifecycle transition (`pkg\u002Fsession\u002Fstatuscard.go`): `idle`, `running` (from prompt acceptance to `agent_end`; detail names the running tool), `error` (last run ended with a provider\u002Fauth error — the message is in `error:`), `no-model` (prompts are refused; the startup \"No models available…\" notice is in `notice:`). Detail is `key: value` lines (`status`, `model`, `tool`, `runs`, `error`, `notice`). Liveness additionally probes the session's input socket, so a reused pid does not resurrect a dead session.\n\n**Note**: when using `-e \u003Cname>` flags to selectively enable extensions, include all required auth extensions too (e.g. `-d auto-namer -d notify` to suppress noise without blocking auth). Using `-e observe` alone blocks auth extensions.\n\nFor multi-pane monitoring: run `fir observe \u003Cid> -f` in one tmux pane and `fir send \u003Cid>` in another.\n\n## Session stores — reading other hosts' sessions (`sessionStores`)\n\nfir writes sessions only to its own store, `\u003Cagent-dir>\u002Fsessions\u002F` (label `local`). Extra **read-only** stores — directories laid out the same way, e.g. a Syncthing copy of another host's `~\u002F.config\u002Ffir\u002Fsessions\u002F` — are configured in the GLOBAL `settings.json` (a project `.fir\u002Fsettings.json` cannot add stores; fir warns and ignores it):\n\n```json\n{ \"sessionStores\": [{ \"path\": \"~\u002Ffir-traces\u002Fzbox\", \"label\": \"zbox\" }] }\n```\n\n`path` must be absolute or `~\u002F…`; `label` defaults to the directory name, must match `[A-Za-z0-9._-]+`, be unique and not `local`. Stores are always read-only (`readOnly: false` is ignored with a warning). A missing directory is skipped, not an error; a path that overlaps fir's own `\u003Cagent-dir>\u002Fsessions\u002F` is dropped with a warning. `FIR_SESSION_STORES=zbox=~\u002Ffir-traces\u002Fzbox[,label=path]` replaces the setting for one run.\n\nWhat sees them:\n- `fir sessions list [--all|--cwd DIR] [--store L|--read-only] [--limit N] [--json]` — TSV `store modified id msgs path cwd name first_message`, newest first, no header (`--json`: one object per line). Archive sessions are listed from each transcript's `.meta.json` only (no `.meta.json`, or modified in the last minute = mid-sync, → skipped); a missing cwd is decoded from the directory name (lossy: `-` vs `\u002F`).\n- `fir sessions dirs [--json]` — `store path readonly exists`.\n- `fir sessions search PATTERN [--kind bookmark|transcript] [--cwd DIR] [--store L] [--since 72h] [--limit 50] [--per-file 3] [-i] [--json]` — Go regexp, bookmarks first then transcripts, newest first (ctx-* logs excluded); hit = `store kind id path:line snippet`. Bookmarks are fast; a whole archive's transcripts take tens of seconds.\n- `\u002Fresume` picker (labelled `[zbox]`), `fir --resume \u003Cid|label:id>`, ACP `session\u002Flist` (title prefixed `[label]`, `_meta: {\"fir\u002Fstore\": label, \"fir\u002FreadOnly\": true}`), ACP `session\u002Fresume`\u002F`load` (paths inside a configured store are accepted; anything outside every store is still rejected), ACP `\u002Fresume`, and `observe_session all=true` \u002F `fir observe --all` (newest 20 as `archived:\u003Clabel>`; `fir observe \u003Cid|label:id>` snapshots an archived transcript).\n\nResuming an archive session **forks** it into the local store (message: \"Session is in read-only store … forked\"); fir never locks, repairs, writes state, cards, sidecars or metadata inside a read-only store — the check lives in the session store itself. The `recall` skill searches every store through `fir sessions`.\n\n## Driving a fir agent on another host\n\n`observe_session` \u002F `send_session` \u002F `stop_session` see only sessions on the local machine. For a session on another host, run the CLI there over ssh (`rexec`) — snapshots need no TTY and always exit:\n\n```\n# start a named, headless-ish agent (tmux is just the process holder)\nssh HOST \"tmux new -d -s w1 -c ~\u002Fproj 'fir --session-name w1'\"\n\nssh HOST fir observe                       # what is running there, with status\nssh HOST fir observe w1 --status --json    # machine-readable status \u002F last error\nssh HOST fir send w1 --wait --timeout 30m \u003C brief.md  # send, wait for the turn, get the reply\nssh HOST fir observe w1 -n 40              # what it did\nssh HOST \"fir send w1 '!new direction'\"    # steer;  fir send w1 --abort  cancels\n```\n\nScraping the tmux screen is a fallback only. For a long-lived human view: `ssh -t HOST 'tmux new -As watch \"fir observe \u003Cid> -f\"'`.\n\n## Environment Variables\n\n{{FIR_ENV_VARS_TABLE}}\n\n## Adding Custom Models\n\nfir's model list is the union of built-in models and anything you add. Three ways to add a model so it shows up in `\u002Fmodel`, `--list-models`, and is selectable as a default:\n\n### 1. `models.json` (preferred — persistent, visible everywhere)\n\nDrop a `models.json` in `~\u002F.config\u002Ffir\u002F` (global) or `.fir\u002F` (project). Add models under the relevant provider — they appear alongside built-ins:\n\n```json\n{\n  \"providers\": {\n    \"amazon-bedrock\": {\n      \"models\": [\n        {\n          \"id\": \"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile\u002Fabc\",\n          \"name\": \"Opus 4.6 (Bedrock app profile)\",\n          \"api\": \"bedrock-converse-stream\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 200000,\n          \"maxTokens\": 8192,\n          \"sweScore\": 73.5\n        }\n      ]\n    }\n  }\n}\n```\n\nThe same file also supports per-provider overrides (`baseUrl`, `apiKey`, `headers`, `authHeader`) and per-model overrides under `modelOverrides`. Most model fields (cost, `contextWindow`, `maxTokens`, `sweScore`, `compaction`, `serverTools`, …) can be set on a custom model or supplied via `modelOverrides` to patch a built-in. `sweScore` (SWE-bench Verified %, 0–100) shows up in the `\u002Fmodel` selector and drives its sort order.\n\n#### `api` — the wire protocol (and its silent failure mode)\n\n`api` selects the **wire protocol**, not the vendor. Legal values are the `API`\nconstants in the external module `github.com\u002Fkfet\u002Fai` (`types.go`, at the\nversion pinned in fir's `go.mod`; re-check with\n`grep 'API =' \"$(go env GOMODCACHE)\"\u002Fgithub.com\u002Fkfet\u002Fai@*\u002Ftypes.go`, aliased in\nfir as `pkg\u002Fai\u002Faliases.go`):\n\n`openai-completions`, `openai-responses`, `azure-openai-responses`,\n`openai-codex-responses`, `anthropic-messages`, `bedrock-converse-stream`,\n`google-generative-ai`, `google-vertex`.\n\nExtensions may register further wire protocols at runtime (`ext-api:*`, `ext:*`).\n\nOpenAI-compatible endpoints (OpenRouter, Groq, local servers, …) take\n`openai-completions` — **not** `\"openai\"`, which is a provider name, not an API.\n\nNeither mistake is caught when the config loads:\n\n- **Unknown `api`** — the model still appears in `--list-models` and `\u002Fmodel`;\n  every request to it fails with `no API provider registered for api: \u003Cvalue>`.\n- **Missing `api`** on a provider fir has no built-in for — validation rejects\n  the *whole* custom-model load (`Provider X, model Y: no \"api\" specified. Set\n  at provider or model level`), so every custom model disappears and\n  `--provider X` reports `Unknown provider \"X\"`. That error text is shown only\n  in the interactive `\u002Fmodel` selector, not by `--list-models`.\n\n`api` can be set per provider and per model: model-level wins, then\nprovider-level, then — for built-in providers only — the API of that provider's\nfirst built-in model. So a custom model on a built-in provider may omit `api`; a\nmodel on a brand-new provider may not.\n\n#### Unix-socket endpoints (`unix:\u002F\u002F`)\n\nA `baseUrl` may use the `unix:\u002F\u002F` scheme to reach an HTTP endpoint over a unix\ndomain socket, e.g. `unix:\u002F\u002F\u002Frun\u002Fboxres\u002Fbifrost.sock\u002Fanthropic`. Rule: the\nsocket path is the URL path up to and including the **first** path segment\nending in `.sock` (`\u002Frun\u002Fboxres\u002Fbifrost.sock`); the rest (`\u002Fanthropic`) is the\nHTTP path, sent with `Host: localhost` (so requests go to\n`\u002Fanthropic\u002Fv1\u002Fmessages`). A `unix:\u002F\u002F` URL with no `.sock` segment is an error.\nWorks for every HTTP-based API (anthropic-messages, openai-completions, …),\nstreaming included.\n\nA custom provider may **omit `apiKey`** when every model's effective `baseUrl`\nis `unix:\u002F\u002F` — access is controlled by socket permissions\u002Fpeer credentials, and\nfir sends no auth header:\n\n```json\n{ \"providers\": { \"bifrost\": {\n    \"baseUrl\": \"unix:\u002F\u002F\u002Frun\u002Fboxres\u002Fbifrost.sock\u002Fanthropic\",\n    \"api\": \"anthropic-messages\",\n    \"models\": [{ \"id\": \"claude-sonnet-4-6\" }] } } }\n```\n\n`apiKey` and header values are resolved the same way: a value starting with `!`\nis run as a shell command and its stdout used; otherwise the value is used as an\nenv var *name* if that variable is set, else taken literally. There is no\n`${VAR}` interpolation inside a string.\n\n#### Provider routing: OpenRouter and Vercel AI Gateway\n\nAggregators route one model id to whichever backend provider they pick. Those\nbackends are **not interchangeable**: they run different serving stacks, so the\nsame id can vary in vision support, reasoning-effort handling, quantization,\ncontext window and latency between requests. If reproducibility matters, pin\nthe backend rather than trusting the default \"best available\" routing.\n\nBoth aggregators are configured through the per-model `compat` block — either\non a custom model under `models`, or on a built-in via `modelOverrides`. It is\na **model-level** key: there is no provider-level `compat`. Routing is emitted\n**only** when that model's `baseUrl` contains `openrouter.ai` \u002F\n`ai-gateway.vercel.sh` respectively, so a stray key elsewhere is inert, not an\nerror.\n\n```jsonc\n{ \"providers\": { \"openrouter\": {\n  \"modelOverrides\": {\n    \"moonshotai\u002Fkimi-k2.5\": {\n      \"compat\": {\n        \"openRouterRouting\": {\n          \"only\":  [\"moonshot\"],            \u002F\u002F exclusive allow-list\n          \"order\": [\"moonshot\", \"together\"] \u002F\u002F try in this sequence\n        }\n      }\n    }\n  } } } }\n```\n\n- `only` — providers exclusively allowed. This is the reproducibility knob.\n- `order` — ordered preference; other providers may still serve as fallback.\n- `ignore` — providers to skip.\n- `allowFallbacks` (default true) — set `false` to fail rather than silently\n  route to a backend outside `only`\u002F`order`.\n- `requireParameters` — only route to providers supporting every parameter in\n  the request (the vision \u002F reasoning-effort mismatch guard).\n- `zdr` — restrict to Zero Data Retention endpoints. `dataCollection`\n  (`\"allow\"` | `\"deny\"`) and `enforceDistillableText` are the related\n  privacy filters.\n- `quantizations` — e.g. `[\"fp8\"]`, to exclude aggressively quantized serves.\n- `sort` — `\"price\"` | `\"throughput\"` | `\"latency\"`, or an object with\n  `by`\u002F`partition`.\n- `maxPrice` — `{prompt, completion, image, audio, request}` USD per MTok caps.\n- `preferredMinThroughput` \u002F `preferredMaxLatency` — a number, or an object of\n  `p50`\u002F`p75`\u002F`p90`\u002F`p99` cutoffs.\n\nKey names are **camelCase** in fir config and are translated to OpenRouter's\nsnake_case on the wire (`allowFallbacks` → `allow_fallbacks`). Fields whose\nOpenRouter schema is a union of a scalar and an object (`sort`,\n`preferredMinThroughput`, `preferredMaxLatency`) are forwarded verbatim without\nlocal validation — a malformed value is rejected by the API, not by fir.\n\nVercel AI Gateway uses `vercelGatewayRouting` with `only` \u002F `order` only; it is\nemitted as `providerOptions.gateway`.\n\nDiscover valid provider slugs for a model with OpenRouter's endpoints API:\n\n```bash\ncurl -s https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fmodels\u002Fmoonshotai\u002Fkimi-k2.5\u002Fendpoints \\\n  | jq -r '.data.endpoints[] | \"\\(.provider_name)\\t\\(.context_length)\\t\\(.quantization)\"'\n```\n\n**Current limit:** none of these are validated locally — an unknown provider\nslug simply routes to nothing and OpenRouter answers with an error.\n\n#### Drop-in fragments: `models.d\u002F`\n\nAlongside `models.json`, fir reads every `*.json` file in a `models.d\u002F`\ndirectory next to it — `~\u002F.config\u002Ffir\u002Fmodels.d\u002F` (global) and\n`.fir\u002Fmodels.d\u002F` (project). Each fragment has the **same shape** as\n`models.json`. This lets tools and agents add a model or provider by\ndropping in a self-contained file, with no edits to a shared monolith.\n\nLoad order and merge semantics:\n\n- `models.json` is the **base layer**; fragments are deep-merged on top in\n  **lexical filename order** — use `NN-` prefixes (e.g. `10-fable.json`,\n  `20-internal.json`) to control precedence.\n- Within a provider: scalar fields (`baseUrl`, `apiKey`, `api`) and\n  `authHeader` are overwritten when a later layer sets them; `headers` and\n  `modelOverrides` maps are key-merged; the `models` array is concatenated\n  and **de-duplicated by `id`, last writer wins**.\n- A fragment alone is sufficient — `models.json` need not exist.\n- A malformed or `providers`-less fragment surfaces a load error (visible via\n  the registry error \u002F `--list-models`), it is not silently skipped.\n\n```jsonc\n\u002F\u002F ~\u002F.config\u002Ffir\u002Fmodels.d\u002F10-fable.json\n{ \"providers\": { \"anthropic\": { \"models\": [\n  { \"id\": \"claude-fable-5\", \"name\": \"Claude Fable 5\",\n    \"contextWindow\": 200000, \"maxTokens\": 64000 } ] } } }\n```\n\nThe catalog is read at process start, and re-read on `\u002Freload` (which now\nalso refreshes the model registry) and when the model selector opens. So a\nnewly added fragment is picked up by `\u002Freload` in a running session, by the\nnext `fir` process, or — for the Poe relay's dropdown — after a relay\nrestart (the relay snapshots the catalog once at its own startup).\n\n#### Catalog overlay (fir-dist)\n\nBelow your config, above the compiled-in catalog, fir merges a **catalog\noverlay**: a `models.d`-shaped document published as a static file at\n`https:\u002F\u002Fraw.githubusercontent.com\u002Fkfet\u002Ffir-dist\u002Fmain\u002Fcatalog-v1.json`. It\nexists so a newly-released model can reach the whole fleet as *data* — no\nbinary release, no redeploy. It can add models, correct metadata, and move a\nprovider's default model (`providerDefaults`). A provider default is only a\nproduct-line anchor: it resolves to the newest registered generation of the\nsame line (e.g. `claude-opus-5` → `claude-opus-5-5` once that model is\nregistered), keeping prefixes, variants and `-preview` tags, skipping dated\nsnapshots. Explicit pins (`defaultModel`, `--model`) are not advanced.\n\nFull precedence, lowest to highest:\n\n```\ncompiled-in catalog  \u003C  fir-dist overlay  \u003C  models.json  \u003C  models.d\u002F*.json\n```\n\n**Your config always wins.** The overlay may not set `apiKey`, `baseUrl`,\n`headers` or `authHeader` at any level (provider, model, or `modelOverrides`) —\nendpoint and auth stay under operator control, so a typo in a published catalog\ncan never redirect the fleet's credentials. It follows that the overlay can only\nadd models to providers fir already knows about.\n\n- Fetched at startup and then every hour for the life of the session, cached\n  at `\u003Cagent-dir>\u002Fcache\u002Fcatalog-v1.json`; a changed catalog is hot-applied, so\n  even a bot process that has been up for weeks follows within the TTL.\n  A binary also embeds a snapshot, so fir works fully offline; every failure\n  (no network, 404, malformed JSON, unknown schema version) is soft and logged\n  at debug, never a degradation.\n- Force a refresh with `\u002Freload`, which refetches ignoring the TTL and\n  hot-applies a changed catalog to the running session.\n- `fir --list-models --verbose` prints each model's origin\n  (`builtin` \u002F `overlay` \u002F `user:models.json` \u002F `user:models.d\u002F\u003Cfile>`) so you\n  can see which layer won.\n\n**Client version pins (`clientVersions`).** The overlay also carries a small,\nclosed set of third-party client version scalars — currently just\n`clientVersions.claudeCode`, the Claude Code CLI version the `anthropic-auth`\nextension advertises as `claude-cli\u002F\u003Cv> (external, cli)`. Anthropic gates new\nmodels on it, so shipping it as data means a gated model and the version pin it\nrequires arrive together in one document, instead of costing a one-line binary\nrelease.\n\n- The extension holds **no version literal**: it declares the template\n  `claude-cli\u002F{clientVersion.claudeCode} (external, cli)` and fir expands\n  `{clientVersion.\u003Ckey>}` in header **values only** (never names, never URLs)\n  at token-request time and after `auth\u002Fmodify_models`. Hot: a published\n  change takes effect within a TTL with no restart.\n- Values must match `^[0-9]+(\\.[0-9]+){0,3}$` and be ≤ 32 bytes, so nothing\n  free-form can ride in.\n- The value compiled into the binary is a **floor**: a published document can\n  only ever *advance* the pin, never roll a host below what it shipped with.\n- To read what a session is advertising: `\u002Freload` names it in its status line\n  (`… (claude-code 2.1.280)`); the committed value is\n  `clientVersions.claudeCode` in `pkg\u002Fmodels\u002Fcatalog-v1.json`.\n- The nightly `model-watch` job reads npm `dist-tags.latest` for\n  `@anthropic-ai\u002Fclaude-code` and proposes a forward-only bump on the same\n  rolling PR as new models; a moved pin alone is enough to open one.\n- **Detecting that a bump is needed.** When an OAuth Anthropic request is\n  rejected with the vendor's gate sentence (`Claude Code X does not support\n  this model; version Y or newer is required`), the error is rewritten to name\n  the pin, its source (overlay vs embedded floor) and the remedy, and a\n  `client-version-gate` record (pin, pinSource, required, model, host, ts,\n  vendor error) is appended to `~\u002F.config\u002Ffir\u002Fdoctor.jsonl` —\n  `doctor_query pattern=client-version-gate` lists them. Every session start\n  prints a one-line warning while any record is unresolved; it clears itself\n  once the effective pin moves strictly past the rejected one (no manual\n  step). `fir doctor client-version-gates` prints the unresolved ones (nothing\n  when clear); fleet converge publishes that as\n  `~\u002Fsync\u002Fshared\u002Ffleet\u002Fstatus\u002F\u003Chost>.gates`.\n\nEscape hatches (env vars):\n\n| Variable | Effect |\n| --- | --- |\n| `FIR_NO_CATALOG_OVERLAY=1` | Disable overlay **fetching**. The embedded snapshot still applies — this pins a host to the catalog its binary shipped with. |\n| `FIR_CATALOG_OVERLAY_URL=\u003Curl>` | Fetch the overlay from somewhere else (staging file, local server). |\n\nPublishing: edit `pkg\u002Fmodels\u002Fcatalog-v1.json` in the fir repo and merge to\n`main`; the `publish-catalog` workflow pushes it to fir-dist. **To roll a\ncatalog change back, re-publish the old content with a fresh `generatedAt`** —\na binary loads whichever of its embedded snapshot and its cache is newer, so\nreverting the commit alone can be a no-op.\n\n### 2. Environment variables (Claude Code parity)\n\nSet `CLAUDE_CODE_USE_BEDROCK=1` and `ANTHROPIC_MODEL=\u003Cid-or-arn>` (plus standard AWS auth — `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`\u002F`AWS_SECRET_ACCESS_KEY`, optionally `AWS_REGION`). At startup fir registers the model under `amazon-bedrock` automatically so it appears in `\u002Fmodel`. Explicit `--provider`\u002F`--model` flags still win.\n\n`ANTHROPIC_MODEL` accepts a regular Bedrock model id (e.g. `us.anthropic.claude-opus-4-6-v1`) or a full Bedrock ARN (e.g. `arn:aws:bedrock:us-east-1:123:application-inference-profile\u002Fabc`).\n\n### 3. CLI one-off\n\n`fir --provider amazon-bedrock --model \u003Cid-or-arn>` works without any config — fir clones a sibling Bedrock model, substitutes the id, and routes through `ConverseStream`. Not persisted; not shown in `\u002Fmodel` until restart with `models.json` or env vars.\n\n## Extensions\n\nExtensions are standalone scripts (Python, shell, etc.) in `.fir\u002Fextensions\u002F` (project) or `\u003Cagent-dir>\u002Fextensions\u002F` (global). They communicate with fir over JSON-RPC 2.0 on stdio and can register custom tools, slash commands, and event handlers.\n\nA Python SDK is provided (`fir_ext.py`). No code changes needed to add an extension — just drop a script in the directory.\n\nDisable an extension with `chmod -x`; a renamed file still loads.\n\nControl loading via `settings.json` `\"extensions\"` allowlist, `--extension`\u002F`-e` flags, or `--no-extensions`.\n\nExtensions can optionally self-restrict by mode using script comment frontmatter (`# modes: ...`), e.g. `tui`, `acp`, or `text`. Mark an extension `# explicit: true` to make it **opt-in only** — it stays discoverable (so `fir -e \u003Cname>` and listings find it) but is **not** auto-loaded; it runs only when named in the allowlist. The shipped `demo` and `hello` example extensions use this.\n\nBuiltin extensions (notify, tmuxspinner, plan_nudger, handoff_nudger, etc.) are embedded in the binary and auto-discovered at lowest priority. Use `fir extensions` to list them and `fir extensions install \u003Cname>` to extract one for customisation.\n\n`handoff-nudger` watches context usage after each turn and prepends a `[SYS_EXT]` note telling you to wrap up and `self_handoff` — auto-compaction only fires at 70% and is lossy, while cache-read cost grows linearly with conversation length. Two triggers: the ceiling, the lower of `atTokens` (500000) \u002F `atPercent` (65) of the window; and a cold prompt cache — 30% of that ceiling (floored at 100000) once the session has been idle past `idleMinutes` (65), since the provider's 1h cache has expired by then and every further turn pays full input on the whole prefix. Tune or disable it in `handoff-nudger.json` (`atTokens`, `atPercent`, `idleMinutes`, `nudgeEvery`, `off`) in `.fir\u002F` or `~\u002F.config\u002Ffir\u002F`.\n\n**Context provenance (`ctx`)** — while the builtin `ctx` extension runs, fir core logs every chunk of each provider request to `ctx-\u003Csession-id>.jsonl` next to the transcript (`kind:src` rows `{turn, addr, source, owner, tokens, hash}` on first sight; one `kind:turn` row per call with `model`, `tokens` and present `addrs`; tokens ≈ bytes\u002F4). Sources: system prompt, AGENTS.md, SYS_EXT section, SYS_EXT, skills index, tool schema, lesson, skill read, tool result, user msg. Chunks ≥100 tokens (and every lesson) carry a stable `[ctx:\u003C5-hex>]` tag (tool results: `[ctx: …]` meta) on the provider copy only — history is untouched and tags are content-derived, so caches hold. The model may call `ctx_used(addrs, why?)` to log chunks it used (`kind:cite` rows). The `ctx` observable card shows tokens · sources · cited (`observe_session ext=ctx`: cited, costliest idle, duplicates). `\u002Fctx [model]` reports across sessions per model sorted by cost. After `reeval_turns` (default 300; `ctx.json` or `FIR_CTX_REEVAL_TURNS`, 0 = off) new turns per model it injects a review prompt asking for trim\u002Fdedupe proposals as a diff — never applied; `pinned` patterns (default system prompt, auth, safety, security, format, copyright) are marked non-trimmable; per-model scores in the global `ctx-state.json` (never the project `.fir\u002F`). Disable with `--extension`\u002Fdisabled-extensions like any extension.\n\n**Extension sections** — each extension may own one persistent section of standing context (`ctx.set_section(text)` \u002F `ctx.clear_section()`), stored by fir at `\u003Cagent-dir>\u002Fsections\u002F\u003Cext-name>.md`. Fir reads that directory itself before the first turn (so `fir -p` sees sections without waiting on extension events) and injects the sections of installed, enabled and trusted extensions as one `[SYS_EXT sections]` message right after the system prompt (cache breakpoint after it; no timestamps, byte-stable). Mid-session changes are re-injected in full as `[SYS_EXT section=\u003Cname> vN replaces vM]` on the next turn. Caps: ~500 tokens per section, ~2000 total; over-cap writes are rejected, never truncated. Disabled extensions keep their file (not emitted); uninstalled extensions' sections are deleted at startup. `\u002Fsections` shows what is injected. The builtin `mood` extension publishes its lessons this way. Fir core also owns one per-session section, `[section=mcp]` (never written to disk): one line per configured MCP server that is unhealthy (needs_auth, token_expired, error, disabled) with the fir command that fixes it, e.g. `MCP server slack: configured, NOT AUTHENTICATED, tools unavailable -> fir command: \u002Fmcp login slack`. It is empty when all servers are healthy; the first turn waits up to 5s for initial MCP connects so it reflects real state, and changes are re-injected next turn.\n\nExtensions can be reloaded via `\u002Freload`; there is no automatic file watching. Reloading extensions rebuilds the system prompt, which might impact prompt cacheing.\n\nExtensions can also register top-level CLI verbs via the `cli_verbs:` frontmatter key — `fir \u003Cverb> [args...]` spawns the extension cold (no session) and routes via a `cli_invoke` JSON-RPC request. The extension drives fir's TTY through `host.println()`\u002F`host.readline()` etc. Built-in subcommands cannot be shadowed; verb collisions are a startup error. `fir observe` and `fir send` are the first users; see `docs\u002Fdesign\u002Fextension-cli-verbs.md` and `docs\u002Fextension-protocol.md` § CLI Verbs.\n\n## Skills\n\nSkills are Markdown instruction files at `.fir\u002Fskills\u002F\u003Cname>\u002FSKILL.md` with YAML frontmatter. They provide specialized workflows the agent can follow. Skills are auto-discovered from project, user, and builtin directories.\n\nUse `\u002Fskills` to list loaded skills, `\u002Freload` to pick up changes. Use `fir skills` to list all skills, `fir skills show \u003Cname>` (or shorthand `fir skills \u003Cname>`) to inspect one — flags `--full` to dump the SKILL.md body, `--path` to print only the file path — and `fir skills install \u003Cname>` to extract a builtin skill for customisation.\n\nSkills can also be invoked directly from the CLI: `fir \u002F\u003Cskill-name> [task...]` rewrites into an initial agent message that points at the named skill and supplies the rest of the positional arguments as the task body. Composes with all flags (`-p`, `--model`, `-c`, …). Bash and zsh completion enumerate `\u002F\u003Cskill>` candidates dynamically from `fir skills list`.\n\n`fir \u002F\u003Cname>` also runs **slash commands**: if no skill matches, the name is resolved against the built-in slash commands (`\u002Fchangelog`, `\u002Fsession`, `\u002Fmcp`, `\u002Fmodel X`, `\u002Ftheme`, `\u002Fskills`, `\u002Fplan`, …) and extension-provided commands, and dispatched exactly as if typed at the interactive prompt. Resolution order is skill first (back-compat), then command. Builtin slash commands are declared once in `pkg\u002Fslashcmd` and shared by the TUI and ACP (each marked mode-only where it is, e.g. TUI selectors like `\u002Ftheme`, ACP's `\u002Fcontinue`); `\u002Fhelp` lists the ones available in the current mode. `fir \u002F\u003Cname>` is TUI-bound, so in headless modes (`-p`, `--output-mode json`, ACP) they fail fast with `command \u002Fx requires interactive mode` rather than being sent to the model.\n\n## External Packages\n\nInstall\u002Fmanage external packages (git repos or local paths) that contribute skills, extensions, and themes:\n\n```\nfir install github.com\u002Fuser\u002Ffir-pack        # install to user scope (\u003Cagent-dir>\u002Fpackages\u002F)\nfir install .\u002Flocal\u002Fpath --local            # install to project scope (.fir\u002Fpackages\u002F)\nfir uninstall github.com\u002Fuser\u002Ffir-pack      # remove a package\nfir packages list                           # list installed packages (source, scope, skill\u002Fext counts, path)\nfir packages update [source]                # pull latest for one or all packages\n```\n\nPackages are stored in `settings.json` under `\"packages\"`. Each entry is a string (`\"github.com\u002Fuser\u002Frepo\"`) or an object with `\"source\"` and optional per-type filters. Installed package skills, extensions, and themes are automatically loaded.\n\n**Agent Plugins.** A package whose root has `plugin.json` is loaded as an [Agent Plugin](https:\u002F\u002Fagent-plugins.org\u002Fspecification) (v1.0.0) instead: `plugin.json` is validated (invalid → the whole plugin contributes nothing), skills come only from `skills\u002F*\u002FSKILL.md` (invalid ones skipped), and `\u003Cplugin>\u002Fmcp.json` servers join MCP config as the lowest layer (user\u002Fproject entries of the same name win, with a warning). Stdio servers run with cwd = plugin root and get `PLUGIN_ROOT` and `PLUGIN_DATA` (`\u003Cagent-dir>\u002Fplugin-data\u002F\u003Cname>-\u003Csource-hash>\u002F`, or `.fir\u002Fplugin-data\u002F…` for `--local`; created only for plugins with stdio servers; kept across updates). `fir.json` and auto-discovery are ignored for plugins. Diagnostics print at `fir install` and in startup MCP warnings. fir reserves the client namespace `dev.fir` (no contents defined). User guide: `docs\u002Fagent-plugins.md`. Code: `pkg\u002Fagentplugin\u002F`.\n\n## Resource Lookup Paths\n\nSkills and themes are discovered from multiple locations. Each\nloaded resource is tagged with an **origin** that identifies where it came\nfrom:\n\n| Origin | Where |\n|--------|-------|\n| `builtin` | embedded in the binary |\n| `user` | `\u003Cagent-dir>\u002Fskills\u002F` (and equivalents for themes) |\n| `project` | `\u003Ccwd>\u002F.fir\u002Fskills\u002F` |\n| `pkg:\u003Csource>` | installed package, e.g. `pkg:github.com\u002Fkfet\u002Ffoo` |\n| `path:\u003Cbasename>` | path supplied via `--skill \u003Cpath>` or the `\"skills\"` settings array |\n\nOrigins are surfaced via the `\u003Cname>__\u003Cid>` form below; they are not\nhierarchical \"wins one, loses another\" priorities.\n\n### Coexistence and Disambiguation\n\n**Same-named skills coexist by default.** A user skill called `release` and a\npackage skill called `release` will both load. Each gets a unique ID of the\nform `\u003Csanitized-origin>__\u003Cname>`, e.g. `user__release` and\n`pkg_github_com_kfet_release_tools__release` (every character outside\n`[a-z0-9_]` in the origin is replaced with `_`, MCP-tool-name style).\n\nThe agent references skills by **bare name when unique**, and by **full ID\nwhen ambiguous**. The `\u003Cavailable_skills>` block in the system prompt\nautomatically switches to the disambiguated form when a name conflict\nexists, and notes this to the agent.\n\n`fir skills list` shows the ID column and tags rows with `[ambiguous]` or\n`[overrides …]`. `fir skills show \u003Cname-or-id>` accepts either form; if a\nbare name is ambiguous it prints the candidate IDs and exits non-zero.\nSlash invocations (`fir \u002F\u003Cname>`) follow the same rule.\n\n### Overriding Another Skill\n\nTo replace another same-named skill instead of coexisting with it, add\n`override:` to your skill's frontmatter:\n\n```yaml\n---\nname: release\ndescription: My customised release flow\noverride: true                  # replace any other skill named \"release\"\n---\n```\n\nTwo forms are supported:\n\n- **`override: true`** — your skill replaces every other skill with the same\n  bare name. Use this for the common case (\"I want my `release` to *be* the\n  release skill\"). When two skills both claim `override: true`, the\n  highest-precedence origin wins (user > project > path > pkg > builtin),\n  the loser stays loaded under its disambiguated ID, and a warning is\n  emitted.\n- **`override: \u003Cfull-id>`** — your skill replaces one specific target.\n  Useful when you want to coexist with some same-named variants but\n  shadow one in particular:\n\n  ```yaml\n  override: pkg_github_com_kfet_release_tools__release\n  ```\n\n  The origin portion may be written either as the raw form (`pkg:github.com\u002F…`)\n  or the sanitized form (`pkg_github_com_…`); both are accepted.\n\nDiagnostics emitted at load time:\n\n- `duplicate-name` (info) — same-named skills coexist; reference by ID.\n- `shadowed` (warning) — a skill was suppressed by an override.\n- `override-conflict` (warning) — multiple skills claimed `override: true`\n  for the same name; the highest-precedence one won.\n\nThe same scheme applies to extensions and themes (rolling out;\nskills are first).\n\n### Settings Path Resolution\n\nPaths in `\"skills\"`, `\"themes\"`, and `\"extensionPaths\"` settings arrays support three forms:\n\n| Form | Example | Resolves to |\n|------|---------|-------------|\n| Absolute | `\"\u002Fopt\u002Fshared\u002Fskills\"` | `\u002Fopt\u002Fshared\u002Fskills` |\n| Home-relative | `\"~\u002Fmy-skills\"` | `$HOME\u002Fmy-skills` |\n| Relative | `\"skills\"` | `$CWD\u002Fskills` (resolved at startup) |\n\n**Relative paths are resolved against the current working directory**, not against the settings file. This is intentional — it makes relative paths portable across projects:\n\n```jsonc\n\u002F\u002F In ~\u002F.config\u002Ffir\u002Fsettings.json (global):\n{\n  \"skills\": [\"skills\"],       \u002F\u002F → finds .\u002Fskills\u002F in any project that has one\n  \"extensionPaths\": [\"extensions\"]  \u002F\u002F → finds .\u002Fextensions\u002F in any project\n}\n```\n\nThis lets you establish a convention (e.g. \"every project keeps skills in `.\u002Fskills\u002F`\") and have fir discover them automatically without per-project configuration. Paths that don't exist are silently skipped.\n\nFor project-specific settings (`.fir\u002Fsettings.json`), relative paths also resolve against cwd (the project root), which is the natural expectation.\n\n## Background jobs in the `bash` tool\n\nBackground jobs started by the `bash` tool (`cmd &`, `nohup cmd &`, `(sleep N; cmd) &`) keep running after the call returns. Output goes to an in-memory pipe, not to files. The result gives a job id. `bash job=\u003Cid>` returns the job's status and the output since the last read, from a 1MB ring buffer; `job=\u003Cid>, kill=true` stops it; `jobs=true` lists jobs. An exited job is forgotten once it has been read. Timeout or abort kills the whole group. At most 10 background jobs run at once; a call that would start an 11th fails with an error listing the running jobs. Surviving jobs are killed (SIGTERM, then SIGKILL) when the fir process exits or before `\u002Freexec`, and their pipes are closed. If fir crashes, the kernel closes the read ends and the jobs die of SIGPIPE on their next write. Jobs do not outlive the session, so for a delayed action wait inside the session, or use `watcher_add`.\n\n## Remote hosts (`remote` extension)\n\nThe builtin `remote` extension gives the agent six tools for working over ssh, so remote work never needs hand-written ssh one-liners inside `Bash`:\n\n- **`rexec(host, command, timeout_s, cwd, detach)`** — run a shell script on a remote host. The script is shipped on **ssh stdin** to `bash -l -s`, never as `ssh host \"bash -lc '...'\"`, so nothing is re-split by an intermediate shell and no escaping is needed. The `timeout_s` bound is a one-line bash supervisor passed as the ssh command — it backgrounds that `bash -l -s` under job control (`set -m`), so the command leads its own process group (TERM then KILL on expiry, exit 124) while still reading your script straight off the ssh channel. **Not** GNU `timeout`, which does not exist on macOS. `detach=True` runs it under `systemd-run --user --collect` (fallbacks: double-forked `setsid`, then `set -m` + `nohup` for macOS) on the remote box and returns a job id.\n- **`rjob(host, id, action)`** — `status` \u002F `log` \u002F `tail` \u002F `kill` for a detached job. State lives entirely in `~\u002F.cache\u002Ffir\u002Frjobs\u002F\u003Cid>.{log,rc,pid}` **on the remote host** — there is no local registry to drift.\n- **`rput` \u002F `rget`** — scp file transfer sharing the same flags and result envelope.\n- **`rtmux(host, action, ...)`** — `ls | new | send | cap | kill`. tmux on the remote host **is** the PTY; every action is a stateless short exec (no held `ssh -tt`). `cap` returns `unchanged: true` when the pane is byte-identical to the last capture, so polling is cheap.\n- **`rhosts(probe)`** — list `Host` stanzas from `~\u002F.ssh\u002Fconfig` (Includes followed), optionally with a parallel reachability sweep.\n\nAll connection flags (`BatchMode`, `ConnectTimeout`, `ServerAliveInterval`, and a `ControlMaster` mux at `~\u002F.ssh\u002Ffir-cm-%C` with `ControlPersist=120`) are owned by the extension. Hosts come from `~\u002F.ssh\u002Fconfig`; there is no fir-side host registry. Every tool returns a discriminated envelope — `outcome` is one of `ok | nonzero_exit | timeout | unreachable | auth_failed | no_tmux | no_target`, with `stdout_bytes` always explicit. Only transport\u002Ftarget failures are tool errors; a remote command exiting nonzero is ordinary signal.\n\n**Self-host detection.** A fleet agent frequently asks for the box it is already running on by alias, and ssh loopback fails with `Permission denied (publickey)` because a host's key is not in its own `authorized_keys`. `rexec` \u002F `rjob` \u002F `rput` \u002F `rget` detect this and run **locally** instead, under an sshd-shaped scrubbed environment (`$HOME` cwd, bare PATH, no `TMUX`\u002F`FIR_*`\u002F`SSH_AUTH_SOCK`, same login shell, same timeout supervisor, plus the `XDG_RUNTIME_DIR` that `pam_systemd` would set so `detach=True` still gets a `systemd-run --user` unit). The envelope then carries `local: true`, and `rhosts` marks the entry `self: true`. The test is not a hostname comparison: `ssh -G` resolves the target, a `ProxyCommand`\u002F`ProxyJump`, a non-22 port, a different target user or an `ip_nonlocal_bind` kernel all bail straight back to ssh, and otherwise each resolved address is `bind()`-tested — an address binds only if it is configured on a local interface. Pass `via_ssh=True` (or set `FIR_REMOTE_FORCE_SSH=1`) when the ssh hop itself is what you are testing. `rtmux` never runs locally: it refuses a self-host outright, because `send-keys` into the local tmux server can land in the agent's own pane — use the `tmux-driver` skill there.\n\nThese tools make teleoperation cheap, which is a trap: for substantial work on another host, `rput` a brief and `rtmux new` an agent over there rather than driving the job one call at a time.\n\n## Key Concepts\n\n- **Sessions** — conversations are persisted and can be continued (`-c`) or resumed (`-r`). Sessions form a tree; double-Escape or `\u002Ftree` navigates branches. `\u002Fsession` (or `Ctrl+S`) toggles a sticky, collapsible session-info overlay above the editor (version, IDs, message\u002Ftoken stats, enabled extensions); toggle it again, or `Ctrl+L`, to dismiss it without leaving anything in the transcript. Use `\u002Fnew [prompt]` to start a fresh session, optionally with an initial prompt that is submitted immediately. `\u002Fnew` (and `Ctrl+N`) drop any queued messages and say so (\"Discarded N queued messages\"); a `self_handoff` instead carries them into the new session, delivered after the handoff prompt — preceded by any user message the old session had not yet answered (\"N unanswered messages carried into the new session\"). **Resuming a file (non-interactive):** `fir --resume \u003Cfile.jsonl> -p '...'` appends the turn to that file (a value is taken without `--at` only when it is an existing file or ends in `.jsonl`). **Forking at an entry (non-interactive):** `fir --resume \u003Csession> --at \u003Centry-id>` (or `--session \u003Cpath> --at \u003Centry-id>`) is `\u002Fresume` + `\u002Ftree` to that entry in a new process: it writes a child session (header `parentSession` points at the source; recorded invocation is inherited) holding the root→entry path and continues there; the parent file is never written. `\u003Csession>` is a path, a file name, or a session id fragment. An entry that leaves an assistant tool call without its result is rejected. The child's history is byte-identical to the parent's prefix, so on the same model\u002Ftools the Anthropic prompt cache hits. A running agent learns its own `session.file`, `session.leafId` and `session.forkAt` (the newest entry `--at` accepts — the leaf, unless the leaf is mid tool call) from `agent_introspect`. ACP: `session\u002Ffork` (capability `sessionCapabilities.fork`) forks at the parent's forkable leaf, or at `_meta.at: \"\u003Centry-id>\"` (advertised as `fork._meta.at: true`); the response's `sessionId` names the child. Each `session\u002Fprompt` response carries `_meta.leafId` (the turn's leaf; advertised as `promptCapabilities._meta.leafId`, omitted if mid tool call) — pass it as `_meta.at` to fork exactly after that turn. Legacy fir-only: `\"at\"` on `session\u002Fload`\u002F`session\u002Fresume` (spec route: `_meta.at`, child in `_meta.sessionId`). ACP `\u002Fresume \u003Cn|path> --at \u003Cid>` does the same in-session.\n  **Subagent with context: fork yourself.** Default way to start a subagent that needs this session's context (instead of a fresh session or an `aside` delegate that re-explores):\n  1. `agent_introspect` → `session.file`, `session.forkAt`.\n  2. In the SAME cwd on the SAME model: `fir --resume \u003Cfile> --at \u003CforkAt> --model \u003Csame> -p '\u003Ctask; return a short summary>'` (ACP: `session\u002Ffork`).\n  3. Use only the child's summary; the parent file is untouched.\n\n  Rules: same model and tool set or the cache misses (an `aside` delegate on a cheaper model always misses); fork promptly (5-min cache TTL); the child inherits the full context, so don't fork near the context limit; children that write files need their own worktree; prefer it when the parent holds >~30k tokens of useful context, use a fresh session for unrelated work. Measured 2026-10-03 on fir 1.25.0: forking a 97.7k-token session gave first-turn cacheRead 96,131 \u002F cacheWrite 711 (~98% cached); on 1.24.4 (before the prompt split) cacheRead was 0.\n- **Re-exec for local build testing** — use `\u002Freexec` to restart into the current binary while preserving the active session, or `\u002Freexec \u003Cpath>` to switch to a specific built binary.\n- **In-place update** — use `\u002Fupdate` to check for, download, and install the latest release, then automatically restart the session.\n- **Compaction** — when context grows large, fir automatically summarizes older messages to stay within the model's context window. Configurable via `settings.json`.\n- **Review & Fix** — use the `review-and-fix` skill to run a one-shot review pass over recent changes. Covers build breaks, security, correctness, test gaps, and simplification (code reuse, quality, efficiency). Fixes all issues found, verifies the build, and commits.\n- **Aside (ephemeral side queries)** — use `\u002Faside \u003Cquestion>` to ask a quick side question using the current session context without adding the exchange to history. The response is shown as a notification and the main conversation is unaffected. Works even while a task is streaming. When given tool calls, `\u002Faside` orchestrates multi-tool workflows ephemerally via `ctx.call_tool()` + `ctx.side_query()` — raw tool outputs never enter the main conversation, only the synthesis is returned. Use `\u002Faside read the 5 largest .go files and summarise` to ask the agent to build and run an aside automatically. The `aside` tool can also be called directly by the agent — including in **agentic delegate mode**: pass `goal` (with `delegate=true`, mutually exclusive with `tools`) and the cheap delegate model drives its own read-only calls (whichever of `read`\u002F`glob`\u002F`grep`\u002F`ls` this session has) in a loop until it can answer, bounded by `max_iterations` (default 8, clamped 1-20), a total tool-output cap and a wall clock; narrow the tool set with `allow_tools`. Agentic mode is delegate-only and never falls back to the executor model. Escalation (`escalate=true`) has a `mode`: `self` (advisor sees the full session, the default behaviour), `strong` (the model set as `\"strong_advisor\"` in `aside.json` — by default the strongest available advisor chain, Fable first — sees only a composed brief — original task, last few turns, the turns named in `refs` (ids or exact quotes), and the question — kept as one append-only, cache-friendly thread per session+model, compacted past ~40k tokens), or `auto` (default: strong when the user asks for it, when `high_stakes=true`, or when self was already consulted on the same problem; else self). Strong falls back to self with a note only when `strong_advisor` is `\"off\"` or no strong model is available. Every aside result (and `\u002Fadvise`) ends with a token-accounting footer — `[in 1.2k · read 48.3k · write 612 · out 900]` — summed over every LLM call the escalation made; `read` vs `write` shows whether it hit the advisor prompt cache. For a subagent that needs this session's context to *do* work, fork yourself (see Sessions) rather than an aside delegate, which re-explores and misses the cache. Advisor replies that are only tool calls (native or written as `\u003Cinvoke>` text) are rejected and retried, never returned; the default advisor chain ends with a non-adaptive model at effort `off` (add one yourself, e.g. `anthropic\u002Fclaude-sonnet-4-5:off`, to an explicit `aside.json` chain); every failed advisor attempt is logged to the doctor log as `advisor_failure` with a `kind`.\n- **Thinking levels** — control reasoning depth: none, minimal, low, medium, high. Toggle with `Shift+Tab` or `--thinking`.\n- **Tool steering** — `\"steeringMode\"` in settings controls whether the agent runs tools one-at-a-time or in parallel.\n- **call_tool bridge** — extensions can call any registered tool (built-in, extension, or MCP) programmatically via `ctx.call_tool(name, params)`. Results are returned directly and never enter conversation history. This enables extensions to build rich orchestration workflows.\n- **MCP channel servers** — MCP servers that advertise the `claude\u002Fchannel` experimental capability can push messages into the running session via `notifications\u002Fclaude\u002Fchannel` notifications. Messages are injected into the agent conversation automatically. The server's `channel_reply` tool is a regular MCP tool. Configure channel servers in `.fir\u002Fmcp.json` like any other MCP server — no special config needed.\n- **MCP configuration** — configure MCP servers in `.fir\u002Fmcp.json` (project) or `\u003Cagent-dir>\u002Fmcp.json` (global, e.g. `~\u002F.config\u002Ffir\u002Fmcp.json`). You can also drop per-server files into the global `~\u002F.config\u002Ffir\u002Fmcp.d\u002F*.json` directory — handy for managing one MCP per file. All sources are merged with precedence (low to high): (1) `~\u002F.config\u002Ffir\u002Fmcp.json`, (2) `~\u002F.config\u002Ffir\u002Fmcp.d\u002F*.json` (lexically sorted), (3) `\u003Cproject>\u002F.fir\u002Fmcp.json`.\n\n  **Field reference — read it, do not recall it.** The complete, authoritative definition of every `mcpServers` entry and every `auth` key lives in `mcp-config-contract.md`, next to this file in the skill directory. It is fir's own source, embedded verbatim, so it can never drift from the running binary. When a user asks what a field does, which fields exist, what a default is, or whether some key is still supported, **read that file** and answer from it. Do not answer such questions from memory of this skill, and do not reproduce the field list here. What follows is the operational half: worked examples, the login flow, and how to choose.\n\n  Three transports are supported:\n  - **stdio** (default) — launches a local subprocess:\n    ```json\n    {\n      \"mcpServers\": {\n        \"filesystem\": {\n          \"command\": \"npx\",\n          \"args\": [\"-y\", \"@modelcontextprotocol\u002Fserver-filesystem\", \"\u002Fhome\u002Fuser\u002Fprojects\"],\n          \"env\": { \"NODE_PATH\": \"\u002Fusr\u002Flocal\u002Flib\u002Fnode_modules\" }\n        }\n      }\n    }\n    ```\n  - **streamable** (recommended for remote) — connects to a remote HTTP MCP server:\n    ```json\n    {\n      \"mcpServers\": {\n        \"my-remote\": {\n          \"transport\": \"streamable\",\n          \"url\": \"https:\u002F\u002Fmy-server.example.com\u002Fmcp\"\n        }\n      }\n    }\n    ```\n  - **sse** — connects via Server-Sent Events (legacy HTTP transport):\n    ```json\n    {\n      \"mcpServers\": {\n        \"legacy\": {\n          \"transport\": \"sse\",\n          \"url\": \"https:\u002F\u002Fold-server.example.com\u002Fsse\"\n        }\n      }\n    }\n    ```\n  - Any server may also declare `roots` — a list of `file:\u002F\u002F` URIs that fir advertises to the server as its filesystem roots, for servers that scope their operations to them:\n    ```json\n    {\n      \"mcpServers\": {\n        \"filesystem\": {\n          \"command\": \"npx\",\n          \"args\": [\"-y\", \"@modelcontextprotocol\u002Fserver-filesystem\"],\n          \"roots\": [\"file:\u002F\u002F\u002Fhome\u002Fuser\u002Fprojects\", \"file:\u002F\u002F\u002Fsrv\u002Fdata\"]\n        }\n      }\n    }\n    ```\n    When omitted (the default) fir advertises a single root: the process working directory. Entries are sent verbatim, so they must be `file:\u002F\u002F` URIs, not bare paths.\n- **MCP files extension (experimental)** — per-server opt-in `\"files\": {\"enabled\": true}` turns on the draft `io.modelcontextprotocol\u002Ffiles` extension, used only with servers that advertise it on protocol 2026-07-28+. Tool arguments marked `x-mcp-file` are shown to the model as `{\"path\": \"\u003Clocal file>\"}`; fir checks `accept`\u002F`maxSize`, sends the file inline (within the server's `maxInlineBytes`) or uploads it to the server's `upload_required` target and retries the call, so neither bytes nor target URLs enter context. Download targets in a result's `_meta` are fetched (size\u002Fdigest checked, 100 MiB default cap via `files.maxDownloadBytes`) into a private temp dir and the saved path is appended to the result. Sending a file needs approval: ACP asks the client with `session\u002Frequest_permission`; TUI and headless sessions have no prompt and refuse unless `files.autoApprove` is set. Targets: https only, no redirects, MCP credential only on `credentials:\"include\"` same-origin\u002F`path` targets, no private addresses; `files.allowedOrigins` restricts targets and is the only way to allow loopback\u002Fprivate or plain-http loopback targets. Design notes: `docs\u002Fmcp.md` (Files extension). Code: `pkg\u002Fmcp\u002Ffiles*.go`.\n- **MCP authentication (OAuth)** — remote (`streamable`\u002F`sse`) MCP servers need **no configuration** to authenticate. fir connects unauthenticated; on a `401` it runs the MCP authorization-spec discovery chain automatically — RFC 9728 protected-resource metadata (from the `WWW-Authenticate` challenge, falling back to the well-known path), RFC 8414 authorization-server metadata, RFC 7591 dynamic client registration — then an authorization-code + PKCE login with the RFC 8707 `resource` indicator. Tokens are stored in `auth.json` under an `mcp:\u003Cserver>` key, bound to the server URL they were minted for, refreshed proactively before expiry and silently on a 401.\n  - fir never opens a browser on its own — the MCP manager holds no login-UI hook at all, so no reconnect cycle or background goroutine can start one. A server that needs a login reports `run: \u002Fmcp login \u003Cserver> (or: fir mcp login \u003Cserver>)`. Log in with **`fir mcp login \u003Cserver>`** from a terminal, or **`\u002Fmcp login \u003Cserver>`** inside a session (TUI or ACP — over ACP a typed `\u002Fmcp login` only completes if the browser is on the agent host; ACP clients\u002Frelays use `authenticate` methodId `mcp:\u003Cserver>` with `_meta.auth.{interactive,sessionId}`, a two-call URL-out\u002Fpaste-back flow documented in `docs\u002Facp-interactive-auth.md`); **`fir mcp logout \u003Cserver>`** \u002F **`\u002Fmcp logout \u003Cserver>`** removes stored credentials; **`fir mcp list`** shows each server's auth state. After logging in out-of-band, **`\u002Fmcp reload [\u003Cserver>]`** re-reads configs and credentials and reconnects servers that are down, so their tools load without a new session. Per-server health (`ok | connecting | needs_auth | token_expired | error | disabled`, config source, last error, tool count, fix) is in `agent_introspect` under `mcp.servers` and pushed to extensions as the `mcp_state` event. Set `\"disabled\": true` on a server to keep it configured but never connect.\n  - Credentials never ride plaintext: a non-`https:\u002F\u002F` server URL is rejected unless the host is loopback (`127.0.0.1`, `::1`, `localhost`). Use `\"auth\": {\"mode\": \"none\"}` for a plain-HTTP server that needs no credentials.\n  - Optional `auth` overrides (all absent by default — a spec-compliant server needs none):\n    ```json\n    {\n      \"mcpServers\": {\n        \"pat-server\":  {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fa.example\u002Fmcp\",\n                        \"auth\": {\"token\": \"${MY_SERVER_PAT}\"}},\n        \"no-dcr\":      {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fb.example\u002Fmcp\",\n                        \"auth\": {\"client_id\": \"abc123\", \"scopes\": [\"mcp:read\"]}},\n        \"no-auth\":     {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fc.example\u002Fmcp\",\n                        \"auth\": {\"mode\": \"none\"}},\n        \"bad-metadata\":{\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fd.example\u002Fmcp\",\n                        \"auth\": {\"authorization_servers\": [\"https:\u002F\u002Flogin.d.example\"],\n                                 \"scopes\": [\"mcp:read\"]}},\n        \"corp\":        {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fmcp.corp.internal\u002Fmcp\",\n                        \"auth\": {\"allow_private_network\": true}},\n        \"okta\":        {\"transport\": \"streamable\", \"url\": \"https:\u002F\u002Fe.example\u002Fmcp\",\n                        \"auth\": {\"client_id\": \"0oa1b2c3d4EXAMPLE\",\n                                 \"redirect_uri\": \"http:\u002F\u002Flocalhost:8765\u002Foauth\u002Fcallback\"}}\n      }\n    }\n    ```\n    **Choosing a mode** (the field reference has the exact semantics; this is when to reach for each):\n    - *Leave `auth` out entirely* — the right answer for almost every server. fir discovers and logs in on its own.\n    - *Add `token`* when the server issues personal access tokens and has no OAuth at all. Bearer mode is inferred; always use `${VAR}` rather than pasting a secret into the file.\n    - *Add `client_id` (+ `scopes`)* when login fails at the **registration** step — the authorization server does not do RFC 7591 dynamic registration.\n    - *Add `authorization_servers`* when login fails at the **discovery** step — the server publishes no RFC 9728 metadata, or publishes wrong metadata.\n    - *Add `redirect_uri`* when login reaches the authorization server but it rejects the callback — the issuer demands an exactly pre-registered redirect and ignores RFC 8252 §7.3 port variance (Okta is the canonical case). Pair it with `client_id`.\n    - *Add `allow_private_network`* when discovery cannot even connect because the issuer is on an internal network behind a private DNS name.\n    - *Set `mode: \"none\"`* only to opt out deliberately — e.g. a plain-HTTP local server that needs no credentials.\n\n    **Common failure modes:**\n    - `run: \u002Fmcp login \u003Cserver>` — expected on first use. fir never opens a browser by itself; the user must run `fir mcp login \u003Cserver>` or `\u002Fmcp login \u003Cserver>`.\n    - Credentials refused before any request — the server URL is not `https:\u002F\u002F` and not loopback. fir will not send a token in cleartext; fix the URL, or use `mode: \"none\"` if there is genuinely nothing to protect.\n    - A pinned `redirect_uri` that \"looks identical\" but fails — it is compared literally, so `localhost` and `127.0.0.1` are different values. Match what is registered character for character, and make sure the pinned port is free at login time.\n    - Browser cannot reach the box's loopback (phone sign-in through a gateway)? Pin an `https` `redirect_uri` plus `callback_listen: \"127.0.0.1:\u003Cport>\"`, or set `FIR_MCP_OAUTH_REDIRECT_BASE=\u003Chttps base>` so fir uses `\u003Cbase>\u002F\u003CP>\u002Fcallback` while listening on `127.0.0.1:\u003CP>`. The proxy must forward the path unchanged. The variable is ignored when `client_id` or `redirect_uri` is set.\n    - `allow_private_network` set but still blocked — a private IP *literal* is rejected upstream with no opt-out. Use a DNS name with a real certificate.\n    - OAuth-only keys in `bearer`\u002F`none` mode are rejected loudly rather than ignored, because the OAuth chain never runs there; remove them or switch mode.\n    - Stale credentials after changing `authorization_servers` are invalidated automatically; tokens live in `auth.json` under `mcp:\u003Cserver>`, and `fir mcp logout \u003Cserver>` clears them.\n\n- **MCP inspection** — use `\u002Fmcp` to see all configured MCP servers with their connection status, transport, capabilities (resources, prompts), and a full list of exposed tools with descriptions. Use `\u002Fmcp \u003Cname>` for detailed info about a specific server. Use `\u002Fmcp reload` to re-read `mcp.json` and `mcp.d\u002F*.json` configs from disk and apply the diff to running servers without a full session reload; it reports collisions when multiple files define the same server name.\n\n## settings.json Reference\n\nAll fields are optional. Nested objects merge recursively; arrays and primitives from overrides win.\n\n```jsonc\n{\n  \u002F\u002F Model defaults\n  \"defaultProvider\": \"anthropic\",        \u002F\u002F Provider name\n  \"defaultModel\": \"claude-sonnet-4-20250514\",  \u002F\u002F Model ID\n                                         \u002F\u002F A pin here beats the provider's own default forever.\n                                         \u002F\u002F \u002Fdoctor warns when the provider has moved on to a\n                                         \u002F\u002F newer generation of the same model line (never rewrites it).\n  \"defaultThinkingLevel\": \"medium\",      \u002F\u002F none, minimal, low, medium, high\n\n  \u002F\u002F UI\n  \"theme\": \"dark\",                       \u002F\u002F Theme name or path\n  \"hideThinkingBlock\": false,            \u002F\u002F Hide thinking blocks in output\n  \"collapseChangelog\": false,            \u002F\u002F Collapse changelog on startup\n  \"turnCache1h\": true,                   \u002F\u002F Anthropic: cache each turn's user message for 1h (TUI\u002FACP; env FIR_TURN_CACHE_1H=0\u002F1 overrides)\n  \"doubleEscapeAction\": \"tree\",          \u002F\u002F \"tree\" or \"quit\"\n  \"autocompleteMaxVisible\": 5,           \u002F\u002F Max autocomplete suggestions shown\n  \"showHardwareCursor\": false,           \u002F\u002F Show hardware cursor\n\n  \u002F\u002F Shell\n  \"shellPath\": \"\u002Fbin\u002Fbash\",              \u002F\u002F Shell for bash tool\n  \"shellCommandPrefix\": \"\",              \u002F\u002F Prepended to every bash command\n\n  \u002F\u002F Compaction\n  \"compaction\": {\n    \"enabled\": true,                     \u002F\u002F Auto-compact when context is large\n    \"reserveTokens\": 16384,              \u002F\u002F Tokens reserved for compaction summary\n    \"keepRecentTokens\": 20000,           \u002F\u002F Recent tokens to keep uncompacted\n    \"maxContextTokens\": 0               \u002F\u002F Hard token cap — compact when exceeded (0 = disabled)\n  },\n\n  \u002F\u002F Branch summary\n  \"branchSummary\": {\n    \"reserveTokens\": 16384               \u002F\u002F Tokens reserved for branch summaries\n  },\n\n  \u002F\u002F Retry on transient errors\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"maxDelayMs\": 60000\n  },\n\n  \u002F\u002F Thinking token budgets per level\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  },\n\n  \u002F\u002F Terminal display\n  \"terminal\": {\n    \"showImages\": true,                  \u002F\u002F Render images in terminal\n    \"clearOnShrink\": false               \u002F\u002F Clear screen when terminal shrinks\n  },\n\n  \u002F\u002F Image handling\n  \"images\": {\n    \"autoResize\": true,                  \u002F\u002F Auto-resize large images\n    \"blockImages\": false                 \u002F\u002F Block all image content\n  },\n\n  \u002F\u002F Markdown rendering\n  \"markdown\": {\n    \"codeBlockIndent\": \"  \"              \u002F\u002F Indentation for code blocks\n  },\n\n  \u002F\u002F Model filtering (glob patterns; empty = all)\n  \"enabledModels\": [\"claude-*\", \"gpt-4*\"],\n\n  \u002F\u002F Extension\u002Fskill\u002Ftheme allowlists (empty = all)\n  \"extensions\": [],\n\n  \u002F\u002F Extension search directories (relative paths resolve against cwd)\n  \u002F\u002F   \"extensionPaths\": [\"extensions\"]  → looks for .\u002Fextensions\u002F in every project\n  \"extensionPaths\": [],\n\n  \u002F\u002F Additional resource lookup paths (resolved per-invocation relative to cwd)\n  \u002F\u002F Use absolute paths, ~\u002F... paths, or relative paths.\n  \u002F\u002F Relative paths resolve against the working directory, making them portable:\n  \u002F\u002F   \"skills\": [\"skills\"]  → looks for .\u002Fskills\u002F in every project you open\n  \u002F\u002F   \"skills\": [\"~\u002Fshared-skills\"]  → always points to the same directory\n  \"skills\": [],\n  \"themes\": [],\n  \"enableSkillCommands\": true,           \u002F\u002F Allow \u002Fskills slash commands\n  \"enableSysExtensions\": true,           \u002F\u002F Allow extensions to prepend [SYS_EXT] context blocks\n\n  \u002F\u002F Agent behavior\n  \"steeringMode\": \"one-at-a-time\",      \u002F\u002F \"one-at-a-time\" or \"auto\"\n  \"followUpMode\": \"one-at-a-time\",      \u002F\u002F \"one-at-a-time\" or \"auto\"\n  \"transport\": \"sse\",                    \u002F\u002F Transport protocol\n  \"serverTools\": [\"web_search\"],         \u002F\u002F Anthropic server-side tools: \"web_search\", \"web_fetch\", \"code_execution\"\n                                          \u002F\u002F You can also use raw types (e.g. \"web_search_20260209\"); model support is controlled per-model in models.json\n  \"serverCompaction\": {                  \u002F\u002F Anthropic server-side context compaction\n    \"enabled\": true,\n    \"triggerTokens\": 150000,             \u002F\u002F When to trigger (min 50000, default 150000)\n    \"instructions\": \"\"                   \u002F\u002F Custom summarization prompt (replaces default)\n  },\n\n  \"debugLog\": {                          \u002F\u002F Debug log (debug.log) rotation\n    \"maxSizeMB\": 100,                    \u002F\u002F Rotate when the file reaches this size\n    \"keep\": 20,                          \u002F\u002F Number of compressed backups to retain\n    \"compress\": true,                    \u002F\u002F gzip backups (debug.log.N.gz)\n    \"checkEveryWrites\": 1000,            \u002F\u002F Stat the file at most once per N writes\n    \"checkEverySeconds\": 30              \u002F\u002F ...or once per this many seconds\n  },\n\n  \"sessionStores\": [                     \u002F\u002F Extra READ-ONLY session stores (global settings only;\n    { \"path\": \"~\u002Ffir-traces\u002Fzbox\",       \u002F\u002F   see \"Session stores\"). env FIR_SESSION_STORES replaces it.\n      \"label\": \"zbox\" }                  \u002F\u002F   label defaults to the dir name; \"local\" is reserved\n  ],\n\n  \"auth\": {                              \u002F\u002F Credential storage (global settings only; see \"Secret storage\")\n    \"storage\": \"file\"                    \u002F\u002F file (default) | auto (OS secret store if it answers a probe,\n                                         \u002F\u002F else auth.json) | keyring (require it). env FIR_AUTH_STORAGE overrides.\n  },\n\n  \"mcp\": {                               \u002F\u002F MCP (Model Context Protocol) behaviour\n    \"toolTimeoutSeconds\": 120            \u002F\u002F Default per-call timeout for model-dispatched MCP tools\n                                         \u002F\u002F (default 120; \u003C=0 disables; env FIR_MCP_TOOL_TIMEOUT overrides).\n                                         \u002F\u002F Extension\u002Fpipe\u002Fwait calls (incl. timeout=-1) are never clipped.\n  },\n\n  \u002F\u002F Internal (managed by fir)\n  \"lastChangelogVersion\": \"\",            \u002F\u002F Last changelog version shown to user\n  \"packages\": []                         \u002F\u002F Package metadata\n}\n```\n",{"name":5,"builtin":49,"override":49,"description":7},true,"main","3a8198a57b67ae81b50d6f20009a3f45afef6a6f","pkg\u002Fresources\u002Fbuiltin_skills\u002Fself",0,"https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir","https:\u002F\u002Fgithub.com\u002Fkfet","https:\u002F\u002Fgithub.com\u002Fkalin-sa","2026-11-13 02:17:59",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":49},[60,63,66],{"tool":61,"command":62},"npx","npx skills add kfet\u002Ffir --skill self",{"tool":64,"command":65},"bunx","bunx skills add kfet\u002Ffir --skill self",{"tool":67,"command":68},"pnpm","pnpm dlx skills add kfet\u002Ffir --skill self","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Fself\u002FSKILL.md",[71],{"path":72,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":75,"sourceHtmlUrl":76},"mcp-config-contract.md","REFERENCE","参考文档",480,"https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Fself\u002Fmcp-config-contract.md",[78,101,121,143,161,187],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":44},"01M6CS290490SZNX8V35TF3N6X","wt","Spawn a fresh fir agent in its own tmux session on its own git worktree. Use whenever the user wants the work — or the conversation about the work — to happen over there, not here. If the user points at this skill, or has directly loaded it in the context, that alone is the cue — spawn, don't inline.","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Fwt\u002FSKILL.md",[],[80,85,86,87,88,89,90,91,92,93,94,95,96,97,98,27,31,99,100],"tmux","git","worktree","git worktree","work","context","user","spawn","session","use","conversation","cue","fresh","it","at","inline",{"skillNo":102,"name":103,"nameZh":6,"description":104,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":107,"publishedAt":44},"01M6CS26KAKJ7VYRQ3RJ31SJ6S","wrap-up","Wrap up work on a project branch — review, fix, and simplify in a loop until clean, then ff-merge to main.","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Fwrap-up\u002FSKILL.md",[],[108,109,110,111,112,113,114,115,89,116,117,118,50,119,120,31],"wrap up","up","wrap","review","merge","simplify","loop","work on","branch","fix","clean","merge to main","project",{"skillNo":122,"name":123,"nameZh":6,"description":124,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":125,"categories":126,"keywords":127,"publishedAt":44},"01M6CS23P85MSVBX11XMFB29PF","tmux-observer","Attach a tmux window to an already-running fir session — observe its transcript and drive its input via tmux send-keys. Use when you need to watch or steer another fir agent.","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Ftmux-observer\u002FSKILL.md",[],[85,128,129,130,93,94,131,132,133,134,135,136,32,31,137,138,139,140,141,142],"tmux observer","observer","transcript","drive","observe","attach","watch","input","via","keys","steer","window","send","when","running",{"skillNo":144,"name":145,"nameZh":6,"description":146,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":147,"categories":148,"keywords":149,"publishedAt":44},"01M6CS21ENQ3D7Z08TDKXHJRR6","tmux-driver","Drive interactive CLIs (python, gdb, etc.) via named tmux sessions with multi-window support. Falls back to fir's built-in PTY driver when tmux is unavailable.","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Ftmux-driver\u002FSKILL.md",[],[85,150,151,152,153,154,155,156,131,157,158,136,159,160,31,139,141],"tmux driver","driver","python","support","sessions","interactive","PTY","back","gdb","multi","with",{"skillNo":162,"name":163,"nameZh":6,"description":164,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":165,"categories":166,"keywords":168,"publishedAt":44},"01M6CS1YWSAH2TQ7S4VK7VKJT6","telegram-bot-setup","Set up a new Telegram bot channel for a fir project — install the telegram MCP, create a per-project state dir, drop in the bot token, wire .fir\u002Fmcp.json with TELEGRAM_STATE_DIR, pair the user, and verify. Use when the user asks to add, configure, or onboard a Telegram bot for a fir worktree\u002Fproject.","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Ftelegram-bot-setup\u002FSKILL.md",[167],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[169,170,171,172,173,42,174,175,87,176,177,178,179,180,181,182,183,94,109,184,120,160,31,185,141,186],"bot","setup","telegram bot","telegram","telegram bot setup","create","state","SET","verify","onboard","configure","json","pair","install","channel","drop","for","new",{"skillNo":188,"name":189,"nameZh":6,"description":190,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":191,"categories":192,"keywords":193,"publishedAt":44},"01M6CS1VSW4C1W1QED8NDHBHZ9","sync","Sync with an upstream source repo — detect changed files, apply equivalent downstream edits, update the baseline, and report any new features or non-trivial changes picked up.","https:\u002F\u002Fgithub.com\u002Fkfet\u002Ffir\u002Fblob\u002Fmain\u002Fpkg\u002Fresources\u002Fbuiltin_skills\u002Fsync\u002FSKILL.md",[],[189,194,195,196,197,109,43,198,199,160,31,200,201,202,203,186],"update","report","baseline","repo","upstream","detect","source","changes","features","apply"]