# Installation This guide is for people and operators who want Hermes Agent to see the OMH skill pack. Normal users should talk to Hermes through Hermes' skill and chat surfaces. Their direct OMH command surface is normally limited to `omh setup`, `omh update`, and `omh doctor`. The broader CLI is deterministic backend infrastructure for Hermes Agent, wrappers, coding agents, automation, and maintainers. AI agents and operators who need a pasteable protocol should use the root [Agent Install Protocol](../INSTALL_FOR_AGENTS.md). That protocol defines what to run, what to report, and what is still unobserved after install. ## Runtime state in Hermes profiles (operator reference) A native Hermes profile must select its own OMH store, or deliberately select an explicitly shared store. Configure the existing plugin setting in that profile's `config.yaml`: ```yaml plugins: entries: omh: settings: omh_home: ~/omh-stores/profile-a ``` Two trusted profiles may explicitly name the same directory. That shares OMH records and home-wide ledgers; it does not make them separate stores. Existing memory principal/admission checks still apply within shared stores. Different directories are not an OS sandbox. The copied plugin and package use the same uncached root resolver, in this order: 1. A trusted programmatic/CLI home pair (also supports offline operations). 2. The active profile's `plugins.entries.omh.settings.omh_home`, with the existing legacy `config.omh_home` fallback. 3. Legacy `OMH_HOME` from Hermes' active credential scope, verified against the host's profile-home-keyed snapshot (its `.env` plus already hydrated external sources), never another routed profile's process environment. Anonymous scope mappings have no owner identity: mismatching/unverifiable values are refused before I/O. Use an absolute profile setting for bindings supplied only through an ad hoc in-memory secret mapping. 4. For standalone/launch-owner operation only, the legacy `OMH_HOME`/`~/.omh` default. The independent OMH CLI has no Hermes dependency and retains its explicit `--omh-home`, `--hermes-home`, and `--scope project` behavior. Importing `hermes_constants` alone does not select native operation. A plain OMH CLI colocated with a compatible Hermes installation still uses process environment variables. Native home/secret scopes or multiplex mode select the native lane. Actual registration through Hermes' `PluginContext` or memory provider collector also marks that **bundle instance** as native: ordinary single-owner memory loading and later callbacks can run without task scopes. That marker records only the execution lane, never a profile, home or secret. Missing scope-probe APIs are ambiguous and fail closed; missing required callable APIs in an active native host are reported as a bounded `RuntimeBindingError` before configuration or store reads, not downgraded to standalone. Internal host import faults still propagate. A routed profile without a binding, an unscoped multiplex call, and blank or malformed settings are unavailable with a binding error. They do not create an empty substitute store. Relative profile settings are anchored at that profile's Hermes home. `~` still names the OS user's home. Prefer absolute paths; `$HERMES_HOME` is expanded against the active profile, but an already-expanded native `${HERMES_HOME}` value that disagrees with that profile is rejected with an absolute-path diagnostic. Validation follows the winning raw configuration leaf, including administrator-managed precedence; a shadowed user template cannot veto a managed absolute setting. Literal paths and setting presence must also agree with the effective native configuration. Unresolved variables are rejected. Evidence `project_root` and `workdir` accept literal absolute or relative paths and the current user's `~`/`~/...`, but not `~other-user/...` or variable references: model paths never query other OS users, credentials or environment values. Explicit empty-string/whitespace home arguments are rejected in both user and project scope rather than silently selecting a default. This intentionally tightens the former empty-string fallback: omit the argument (`None` in Python) to request defaults. Blank configured native bindings never inherit ambient state. Filesystem failures while resolving runtime homes are also bounded `RuntimeBindingError`s. Hooks classify failed binding before observation I/O; pre-tool binding failure blocks the call because its rules cannot be checked. Direct runtime readers still raise on unreadable state instead of claiming idle. Evidence child binding failure returns an error without spawning a command. Native tools/hooks do not treat home-looking model arguments or observation metadata as permission to choose another store. Legacy model-facing home fields are explicitly rejected by native tools before observation or store I/O, even when blank or equal to the active home; omit them. Their schemas retain and label standalone operator overrides, and trusted offline APIs remain separate. Standalone observations honor explicit top-level operator homes in both package and copied-bundle paths; nested observation metadata is never store authority. Provider and optional observer/egress/browser instances bind before their first I/O; providers refuse reinitialization with another Hermes home. Project recall uses the host's logical cwd rather than a multiplex launch repository; absent/deleted logical context selects no project. Evidence subprocesses receive only the existing minimal non-secret environment plus the resolved OMH/Hermes homes. A separately launched TUI widget must likewise receive its owning process's home pair; Python context scopes do not cross process boundaries. Runtime homes are canonicalized (including symlinks) for store identity and per-home guards. Core project-store paths were already canonicalized; native logical cwd is passed through without replacing it with the process cwd. Stable project identity and bound absent-cwd behavior are unchanged. Repository-scoped artifact defaults are explicit: | Invocation | `omh_home_named` | `project_artifact_dir` | | --- | --- | --- | | Standalone or colocated OMH CLI, no explicit home | false | Logical repository `.omh/`, or user store outside a repository | | Ordinary single-owner native profile | false | Logical repository `.omh/`, or profile-selected store outside a repository | | Routed native profile (multiplex or a non-launch home) | true | Profile-selected OMH store; never a launch repository | | Trusted explicit OMH home/pair | true | Explicit OMH store, including inside a repository | When logical cwd is absent there is no inferred repository; artifacts use the selected store. Explicit `--scope project` still selects the logical project's `.omh`/`.hermes`; without a logical cwd it fails rather than borrowing process cwd. Naming a runtime store does not change the project's stable identity. This binding uses native `hermes_constants` profile APIs, `agent.secret_scope` (including `build_profile_secret_scope`), `agent.runtime_cwd`, `hermes_cli.managed_scope.load_managed_config`, and the read-only config loader plus its strict readability validator. A present but incompatible/broken host is not treated as standalone. Setup/profile synchronization does not stamp one profile's store into every child or migrate data; configure each routed profile explicitly before activation. Existing child settings are preserved. Maintainers can run the opt-in synthetic real-loader suite with a Hermes checkout's `scripts/run_tests.sh`, passing this repository's `tests/native/test_profile_runtime.py`, `tests/native/test_review_native.py`, and `--native-source=/absolute/path/to/hermes`. Use an explicit pytest root/cache inside the test workspace. The ordinary OMH unittest suite needs no Hermes installation. These tests distinguish actual turn-start behavior from positive recall after the native background queue; this change does not alter upstream turn-start pack invalidation or claim live model delivery. ## Command Audience | Audience | Normal interaction | | --- | --- | | Person using Hermes | Ask Hermes for the result in natural language. Run `omh setup`, `omh update`, or `omh doctor` only for local maintenance. | | Hermes Agent or wrapper | Route requests and call structured chat, coding, runtime, memory, and evidence commands behind the conversation. | | Coding agent or automation | Consume prepared contracts, record observed evidence, and run scoped control-plane commands. | | Maintainer or advanced operator | Inspect catalogs, harnesses, release checks, fixtures, and machine-readable payloads. | Commands outside the three human defaults can still be run manually for integration, debugging, or maintenance. When this guide shows them, treat them as agent, wrapper, operator, or maintainer references rather than prerequisites for using OMH. ## Quick Start > **Publication status:** Homebrew, Bun, and npm package-manager installs are > public as of v1.0.6. Clean installation and `omh update` were observed for > each package-manager path in isolated release QA. Choose one installation path. The package-manager paths install the same `omh` command as the platform installers. ### Homebrew ```sh brew install rlaope/tap/omh ``` ### Bun (recommended) ```sh bun install -g oh-my-hermes ``` ### npm ```sh npm install -g oh-my-hermes ``` ### Universal installer (macOS/Linux) ```sh curl -fsSL https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.sh | sh ``` ### Windows (PowerShell 5.1+) ```powershell irm https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.ps1 | iex ``` Windows npm/Bun launcher support is covered by the Windows CI suite, including packed-tarball installation and CLI smoke checks. The PowerShell installer remains the native Windows alternative. ### Set up OMH After any installation path, install the managed skills and register them with Hermes: ```sh omh setup ``` ### Keep every installed layer current The same command updates every supported installation path: ```sh omh update ``` For Homebrew, Bun, and npm installs, the launcher records the owning package manager. `omh update` runs that manager's native upgrade first, then re-enters the newly installed `omh` command. The curl and PowerShell installers use the same flow through their isolated managed virtual environment. After the command package succeeds, the re-entered command refreshes managed skills, an already installed plugin bundle, and existing Hermes registration. On a machine that never completed `omh setup`, `omh update` bootstraps the full OMH TUI surface instead of skipping it: it installs the plugin bundle, registers and enables OMH in the Hermes config (activating the skin), installs the TUI widget, and seeds `~/.omh/routing/model-chains.json` — update and setup converge on the same machine state. One deliberate opt-out is honored: after `omh uninstall --registration-only` the plugin directory stays in place, so update never re-registers a machine whose owner removed the registration on purpose. An explicit `--source` or `--from-skills-dir` remains a workflow-content-only operation, and `--dry-run` never changes the command package. A source checkout or other unmanaged Python environment is not rewritten implicitly; the result reports the supported installer command instead. On installer-managed installs (the curl and PowerShell paths), the update is staged rather than applied in place. OMH builds the new command environment and its workflow pack off to the side, smoke-tests them, and only then swaps them in with a single pointer move. You either keep the version you had or get the new one; there is no half-updated state in between, even if the machine loses power mid-update. If the new version fails its post-activation check, OMH puts the previous one back automatically and says so. To go back deliberately after a bad update, run: ```sh omh update --recover-known-good ``` See [Staged installer updates and recovery](#staged-installer-updates-and-recovery) for what is on disk and what the JSON payload reports. ### Verify or troubleshoot the installation Run doctor separately after setup: ```sh omh doctor ``` First-run expectation: 1. Your chosen package manager or installer exposes the `omh` command. 2. `omh setup` installs the managed skills and records safe defaults. 3. `omh doctor` checks local registration and points to the next repair action. 4. You restart or reload Hermes Agent. 5. You ask Hermes normally, for example: `I want to safely add a feature to this repo.` By default, `omh setup` installs the **full** skill profile: every packaged skill, the ULW engines included — installing OMH means getting OMH. Pass `--core` for the lightweight footprint (the doctor health floor plus the chat/plan/status/handoff essentials); see [Skill Profiles: Core vs Full](#skill-profiles-core-vs-full) for the context-weight trade-off each choice makes. You do not need to know or name a workflow. The quickstart card offers representative natural-language starters from the locally tested request corpus and tells the wrapper which workflow and next action each starter should expose. An adapter that needs an explicit workflow can still use `Use OMH request-to-handoff for: I want to safely add a feature to this repo.` First-value packs are the stronger first-use paths once setup is done: - **Frontend Rescue** for natural frontend layout, anti-AI polish, responsive repair, accessibility checks, and visual QA handoff. - **Repo First-Win** for mapping a new repository and finding the first safe valuable improvement. - **Failure-to-Fix** for failing deploys, Pages, CI, DCO, builds, and tests. - **Visual Deliverable** for polished PR, release, report, deck, PDF, or image summary packages. - **Toolbelt Readiness** for local CLIs, MCP hosts, credentials, connectors, and executor runtime readiness. - **CTO/Product Loop** for roadmap, architecture, launch, QA, security, and operations tradeoff review. These packs prepare routes, handoffs, and evidence boundaries. They do not claim execution, visual QA, CI, deployment, publication, credential validity, or merge evidence until Hermes or the selected runtime observes those steps. If the next step is still unclear, ask Hermes: ```text what should I do next with OMH setup? ``` Hermes/wrappers can answer with the same compact quickstart card without asking for shell command approval. The backend command is: ```sh omh quickstart ``` `omh quickstart` prints the compact first-use card instead of a deep diagnostic dump. It reads the current doctor/probe state, suggests the next Hermes chat prompt, and separates local readiness from evidence that still has to be observed by Hermes or a wrapper. The JSON form is `omh_quickstart_card/v1` and is the same card that `omh chat interact` returns as `chat_response.kind == quickstart` for setup/first-use questions: ```sh omh quickstart --json ``` If the user asks what OMH is or how to use it, wrappers can answer with `chat_response.kind == context_brief` and `[omh] context` before opening the full workflow picker. This keeps the first explanation conversational while still exposing `omh_context_brief/v1` for adapters that want structured lanes, rules, and boundaries. ## Guided Model Setup Normal users can ask Hermes **set up my models**. The CLI in this section is an **agent/maintainer** configuration and diagnosis surface, not a prerequisite for installing or using OMH. The guided flow is deliberately staged: 1. **Inspect.** `omh setup --model-setup` scans bounded, allowlisted metadata roots for Codex, Claude Code, Hermes, OpenCode, OMO, `pi`, and `senpi`. `pi` and `senpi` are host CLIs in the OMO runtime family. Discovery does not open auth files or call providers. It parses bounded local session/config records, which may contain prompt or tool-result fields, but emits only the allowlisted provider, model, variant, timestamp, and source identifiers; it never returns or persists transcript prose or credential values. 2. **Confirm active.** Prior session/config metadata is only `observed_before`. A model becomes `confirmed_active` for this flow only through an explicit `--confirm-model PROVIDER/MODEL` choice. This is still user-declared local configuration, not entitlement, credential validity, quota, or execution. 3. **Preview.** Repeated `--model-alias ALIAS=MODEL` values produce exact Hermes `model.aliases` changes and a digest-bound preview. Existing aliases remain user-owned; collisions fail closed unless separately allowed. 4. **Apply.** No preview writes. An apply requires both `--apply-model-config` and the preview's `--model-config-digest`; an interactive flow asks after showing the preview. Hermes' own `config set` command owns the mutation. 5. **Verify.** The adapter re-inspects the native Hermes alias and reports a verified receipt. Preview, apply, and verification remain separate states. Agent/maintainer preview example: ```sh omh setup --model-setup \ --confirm-model openrouter/qwen3-coder \ --confirm-model google/gemini-3.1-pro \ --model-alias main=openrouter/qwen3-coder \ --no-interactive --json ``` Show the resulting `steps.model_activation.preview.changes` and `config_digest` to the user. Apply only after approval by repeating the same arguments with: ```sh --apply-model-config --model-config-digest ``` An explicit unavailable model returns `choice_required` and never silently falls through. A missing recommended model is different: setup remains usable and ordered recommendation chains skip missing entries in favor of a confirmed compatible alternative. Qwen and Gemini therefore remain valid user-selected alternatives even when they are not shipped category heads. If no candidate is confirmed for the selected category, role-slot, and domain chains, the resolver consults one shared final order: Claude Opus 5, then GPT-5.6 Sol. These names remain editorial candidates filtered through caller-confirmed metadata; they do not prove subscription, entitlement, authentication, or runtime readiness. If no candidate is confirmed anywhere, the resolver records `owner_default`; Hermes or the selected external owner keeps its native default model, setup completes with `status: defaulted`, and no model-config write is prepared or applied. ### Editable recommendation categories The shipped catalog is editorial policy, not benchmark output: | Surface | What it is for | Shipped editable order | | --- | --- | --- | | Hermes `main` suggestion | The session's own model | Kimi K3, Claude Fable 5.1, Claude Opus 5, GPT-6 Astra (`xhigh`), GPT-5.6 Terra (`high`) | | `ultrabrain` | Deepest reasoning | GPT-6 Astra (`xhigh`) | | `deep` | Strong default tier | GPT-5.6 Terra (`high`), DeepSeek Flash (V4.1) (`high`) | | `architect` | Architecture and system design | Claude Fable 5.1 (`xhigh`), GPT-6 Astra (`xhigh`), Kimi K3 (`xhigh`) | | `unspecified-high` | Default working model | Kimi K3 (`medium`), Claude Opus 5 (`medium`) | | `unspecified-low` | Cheaper fallback | GLM 5.3 (`low`), DeepSeek Flash (V4.1) (`low`), Claude Opus 5 (`low`) | | `quick` | Short tasks | GLM 5.3 Flash (`low`), Kimi K3 (`low`), GPT-5.6 Luna (`low`), Claude Fable 5.1 (`low`) | | `writing` | Prose and docs | Kimi K3 (`medium`), Qwen3-Coder (`medium`), Gemini 3.1 Pro (`medium`) | | `visual-engineering` | Frontend and visual | Claude Fable 5.1 (`high`), Kimi K3 (`high`) | | `artistry` | Unconventional work | Gemini 3.1 Pro (`high`), Claude Fable 5.1 (`high`), Kimi K3 (`high`) | | `capable` | Strong general work | Claude Fable 5.1 (`medium`), Claude Opus 5 (`medium`), Kimi K3 (`medium`), GLM 5.3 (`medium`) | | `simple-work` | Small everyday tasks | GPT-5.6 Luna (`low`), DeepSeek Flash (V4.1) (`low`), Claude Haiku 4.5 (`low`) | | `deep-work` | Long tasks at frontier depth | GPT-6 Astra (`high`) | | `x_platform_data` affinity | X-platform data affinity | Grok Code Fast, Kimi K3, Gemini 3.1 Pro | | Shared final order (`last_resort.any`) | Last resort when a chain is exhausted | Claude Opus 5, GPT-5.6 Sol (`medium`) | Chain customization is a config edit, not a source edit — bare `omh model-chains` (or `omh model`) opens an arrow-key picker on a terminal: one row per category, up/down to move, left/right to step the head model through the aliases chains name today, `-`/`+` to step its effort, `d` to restore the shipped default, Enter to save and `q` to leave the file alone; inside the Modern TUI, `/omh-model` opens the same picker as a modal over the transcript (Hermes' own `/model` picks the session model and keeps its name; the two edit different things); `omh model-chains show` prints the current per-category state, `omh model-chains interview` walks every category with numbered choices — the interactive `omh setup` offers that walk as its last question, default No, and a "no" leaves the seeded defaults in effect — and `omh model-chains set "model[:effort], ..."` is the scriptable write (agents included). ![Bare omh model-chains: one row per category with its head model, effort bar and state; the cursor row shows the left/right and -/+ handles](../assets/model-chain-picker-cli.png) All of them edit the same document: `omh setup` seeds `~/.omh/routing/model-chains.json` (`mixture_chain_overrides/v1`) with an empty `categories` object, meaning the shipped defaults above stay live and keep updating with `omh update`. A category written into that file replaces its whole chain — for delegation routing, `omh_delegate_route` fallback walks, and HUD category labels alike — until the user removes it. An invalid document is ignored whole (defaults apply) and reported by `omh_delegate_route` `action=status` as `chain_overrides: invalid: ...`. Next to those two documents, `omh_delegate_route` maintains `~/.omh/routing/route-provenance.json` (`delegation_route_provenance/v1`): a capped history of the routes it prepared (head, explicit, fallback, chain exhaustion, clear) that the HUD uses to label a fallback lane as a fallback, an exhausted chain as `category(model inherit)`, and a lane routed to the model the parent session itself runs as `category(model =parent)` — one `category(model tag)` shape for every lane, where the category names the lane and only the parenthesized model and state token move. A child on the parent's model with no route record at all is the plain `inherit(model)`: inherit is not a category. It is written automatically, carries its own `claim_boundary` (prepared routes only, never dispatch evidence), and is safe to delete — an absent or invalid file only means HUD rows fall back to plain category projection. A fourth sibling, `~/.omh/routing/dispatch-models.json` (`omh_dispatch_model_preferences/v1`), applies to a different surface: `omh coding fanout dispatch`'s `--model` fallback for a spawned agent CLI, used only when a unit's prepared handoff routed no model at all. It is operator-edited (the interactive `omh setup` maestro question seeds it empty on an explicit "yes"; nothing else writes it). See `docs/FANOUT.md` (Dispatch-model preference) for the schema and the `claude-code`/`codex` behavior it fills the gap for; on `omh coding run` it sits below that command's own `--model` flag and any routed handoff model, above only the executor CLI's own default. A fifth sibling, `~/.omh/routing/category-maestro.json` (`omh_category_maestro/v1`), is the Maestro lane's own category dial — the same category vocabulary as the mixture above, applied to the dispatched `codex`/`claude-code` CLIs. `omh coding category-maestro show` prints the effective category → model table (operator overrides marked, invalid pieces named), `omh coding category-maestro set ...` is the scriptable write, `omh coding category-maestro clear` restores a built-in chain, and `omh coding category-maestro interview` walks it guided — the interactive `omh setup` offers that walk right after the maestro question. The file's presence is the opt-in: machines without it keep byte-identical routes, and routes resolved against it record `catalog_kind: "operator_category_config"` plus the config fingerprint in the frozen contract. Catalogless profiles (for example `omo-runtime`, host CLI `pi`/`senpi`) are deliberately not configured here — their categories resolve from the locally-derived model catalog (omo config). See `docs/FANOUT.md` (Category-maestro) for the full rules. ### Reaching models through a provider Chains name models the way a person says them (`glm-5.2`, `kimi-k3`). A host that reaches models through a provider usually needs two different values instead: a provider id, and that provider's own model string, which is often namespaced like `vendor/model-name`. Which provider serves which model, and under what name, belongs to one account — so OMH ships no routes and hardcodes no provider. Which providers and subscriptions a machine holds is a third, separate question. The interactive `omh setup` asks it as one ticked list of the services you have an account, key, or login for: every provider family, plus each provider id in Hermes' config (`providers.` and `model.provider`, except `auto`) and each builtin provider whose key NAME appears in `$HERMES_HOME/.env` or the environment (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...; values are never read), plus OpenGateway — OMH's own gateway, offered whether or not `OPENGATEWAY_API_KEY` is set yet, because a row that appears only once the key exists is a row nobody discovers. Rows found on the machine arrive ticked, because a found config key or variable name is a sensible default — it is not proof of a working account, so every ticked row can be cleared and only what you leave ticked is recorded. The list's last row is **Skip — leave everything as it is**. Choosing it writes nothing at all: no entitlement document, no dispatch-model seed. An earlier record stays as it is, and the providers Hermes is linked to keep counting on their own (see below), so the machine behaves exactly as it does for someone who never answered — which is not the built-in chain order unless nothing is linked. It is exclusive — ticked beside a provider the prompt asks again rather than guessing which half you meant — and it is never pre-ticked. After the list you can type any further provider id (that one is asked for its kind, since OMH knows nothing about it), and, when the Claude Code CLI is on PATH, you are asked whether you have a Claude Code subscription. The answers are recorded in `~/.omh/routing/providers.json` (`provider_entitlements/v1`): ```json { "schema_version": "provider_entitlements/v1", "providers": {"og": "gateway", "zai": "zai"}, "subscription_clis": ["claude-code"] } ``` You do not have to answer for the common case. OMH reads the providers Hermes is already linked to and counts them on its own, the moment they are linked: the ids under `providers` in `auth.json` and the `credential_pool` entries Hermes itself counts (a `hermes auth` device-code or PKCE login, a manual add, an `env:`-seeded row whose variable is still in `.env` — never a credential borrowed from another CLI), every `providers.` key and `model.provider` in `config.yaml`, and the API-key variable NAMES Hermes' registry lists, read from `$HERMES_HOME/.env`. A key exported only in a shell is not counted (Hermes did not record it; the interview below still offers it). Only ids, names, and a pool row's `source` label are read — never a token or a key — and nothing is invoked. A registry id carries its vendor family (`openai-codex`, `zai`, `kimi-coding`); an id OMH cannot place is your own endpoint and counts as a `gateway` unless its `base_url` is a loopback address (LM Studio, a local Ollama), which is left out; a Hermes provider whose models the catalog never describes (MiniMax, StepFun) is ignored rather than guessed, and so is `CLAUDE_CODE_OAUTH_TOKEN`, which Hermes' registry marks implicit. The recorded document sits on top: its kind wins for an id both name, it is the only source of `subscription_clis`, and a linked row you untick in the interview is written as `excluded_providers` and stops counting — the one way short of unlinking it from Hermes. Beyond that neither side removes the other's providers. `omh model-chains show`, the CLI picker, and `/omh-model` list the providers counted, each with where it was found (`login`, `config`, `env`, or `recorded`). With linked providers found or that document present, every chain is reordered so the entries such a provider can serve lead and the rest follow; nothing is removed, so a wrong answer costs one rejected fall-through, never a missing model. A gateway, `openrouter`, `opencode`, or `unknown` kind serves every family; any other vendor kind serves only the models whose editorial candidates name it; an explicit route in `model-providers.json` decides before either. The same reordered chains feed `omh_delegate_route`, the HUD labels, and `omh model-chains show`, which marks a reordered chain. A confirmed Claude Code subscription is a Maestro-lane entitlement: Hermes cannot spend it, so its only effect is seeding the Claude Code `--model` preference in `dispatch-models.json` when none is set (a Codex login is spent by Hermes' own `openai-codex` provider and is counted under providers the moment `hermes auth` records it). `--yes`, `--json`, and runs without `--interactive` on a non-TTY ask nothing and write nothing; rerun `omh setup` interactively to answer again (existing answers arrive pre-ticked) or edit the file. Supply wire-id routes in `~/.omh/routing/model-providers.json` (`model_provider_routes/v1`), a sibling of the chain document: ```json { "schema_version": "model_provider_routes/v1", "models": { "glm-5.2": {"provider": "my-gateway", "model": "z-ai/glm-5.2"}, "kimi-k3": {"provider": "my-gateway", "model": "moonshotai/kimi-k3"} } } ``` ### Token prices The cost figures OMH shows when a host records none are ballparks from a shipped table, and what you actually pay is not: a gateway applies its own markup, an enterprise contract is not the list price, a free tier bills nothing, and vendors reprice. Put your own rates in `~/.omh/routing/model-prices.json` (`model_price_overrides/v1`): ```json { "schema_version": "model_price_overrides/v1", "models": { "claude-fable-5-1": {"input_per_mtok": 8.0, "output_per_mtok": 40.0}, "grok-code-fast": {"input_per_mtok": 0.2, "output_per_mtok": 1.5}, "my-free-tier-model": {"input_per_mtok": 0, "output_per_mtok": 0} } } ``` A model listed here uses your rate; a model not listed falls back to the shipped ballpark, and a model neither prices reports no cost at all rather than claiming it was free. `cache_read_ratio` is optional and defaults to the shipped ratio for that model. Zero is a real rate, not an absent one, so a free tier can say so. Validation is strict and atomic, like its siblings: an invalid document is ignored whole rather than half-applied. A recorded cost from the host is never replaced by any of this — overrides only reach the approximation that fires when nothing was recorded, and an approximated figure still renders with its `~` marker. A row whose figure came from your own rate is marked `cost_override` beside `cost_approximate`, so a number you chose can be told apart from our shipped ballpark. "Nothing was recorded" covers two shapes, both of them the host declining to state a cost: no cost provenance at all, and Hermes' own `unknown` status, which it stamps whenever its pricing produced no amount (`agent/usage_pricing.py:549`, persisted into the usage table by `agent/turn_usage.py:236,257`). Every child served through a custom gateway provider carries that status, because Hermes prices only the routes it has rates for. A row where the host did record an outcome — `included`, a billed zero, any word it chose — keeps that figure untouched, and in a session whose rows mix the two the recorded outcome is what the row reports. When the model has no rate on either side, the row keeps rendering `$0.0000 (unknown)` rather than gaining a figure OMH cannot support. Every shipped rate carries the vendor page it was read from and the month, so a reader can tell a current price from one that drifted. A dispatch that inherits its provider is checked before the route is written: if the session's own provider cannot serve the model being pinned — the catalog records which provider families serve each model — the route is refused, naming the inherited provider and which of your recorded or linked providers could serve it instead. The check only refuses what is known wrong. A provider neither recorded nor linked, a model the catalog never described, and a multi-vendor relay (which serves every family) all leave the answer unknown, and unknown dispatches unchanged. An alias listed here dispatches as that provider's model; an alias not listed dispatches unchanged with no provider, which is what a direct-billing host wants — the file is optional and absent by default. A `provider` passed explicitly with its wire `model` to `omh_delegate_route` outranks any stored route. Provider and model change atomically; partial pairs and providerless wire-shaped models fail before Hermes config is mutated. Fallback translates an active exact provider/wire pair back to one alias and requires the category when that alias has multiple chain origins. Validation is strict, token-only, and atomic: an invalid file is reported by `action=status` and blocks set or fallback rather than silently inheriting a parent provider. `action=status` and fallback results expose the complete `alias`/`provider`/`model`/`reasoning_effort` shape. HUD rows use the same configured mapping for labeling and mark the provider source as `model_provider_routes`; that configured metadata is not provider execution or credential evidence. The X/Grok row is a static, editable affinity for work explicitly declaring X platform data. It is not a measured capability, performance, or availability claim, never removes another candidate, and never overrides an explicit user choice. CCAPI for Claude and Apitopia for Kimi are preferred provider-family metadata only. They are not bundled or probed integrations and are considered only when the user declares the corresponding local route active. OMH never copies their tokens or keys. Agents and maintainers can replace named chains with a secret-free `model_recommendation_overrides/v2` JSON file. Only the existing category, `main` role, `x_platform_data` domain, and shared `last_resort.any` keys are accepted; named chains replace rather than merge with shipped order. Legacy v1 documents remain accepted but cannot define `last_resort`. For example: ```json { "schema_version": "model_recommendation_overrides/v2", "categories": { "deep": [ { "model_alias": "qwen3-coder", "model_family": "qwen", "preferred_provider_families": ["openrouter"], "reasoning_effort": "high", "reasoning": "Local editorial choice for this installation." } ] }, "last_resort": { "any": [ { "model_alias": "claude-opus-5", "model_family": "claude", "preferred_provider_families": ["anthropic"], "reasoning": "Local final metadata selection." } ] } } ``` The agent/maintainer routing preview accepts it with `omh coding model-route --executor hermes --from-inventory --recommendations /path/to/overrides.json --json`. Override files cannot carry credential, secret, token, password, or provider-configuration fields. ### Hermes-native and Maestro ownership Hermes-native routing resolves a reviewed alias/provider/model binding and keeps native skill, Kanban, and `delegate_task` execution in Hermes. It does not cross Maestro. Maestro is the external handoff boundary for Codex, Claude Code, OMO, OMC, OMX, and generic profiles. It projects an ordered eligible recommendation chain, coordinates existing prepared handoff/status adapters, and rejects Hermes as an external profile. Maestro does not execute work, write Hermes aliases, own credentials, or convert a prepared handoff into observed evidence. External owner observations must still be recorded by the selected executor or wrapper. For offline diagnosis, agents and maintainers can run: ```sh omh coding model-routing status omh coding model-routing status --json ``` The report separates confirmed models from discovered-only metadata, Hermes aliases from Maestro category readiness, and owner-learning state from both. Its missing recommendation heads are advisory. To clear only one learned owner preference, an agent/maintainer may run `omh coding model-routing reset --route-family `; this does not alter aliases, recommendations, providers, or credentials. ### External coding CLI handoff (claude-code / codex) **Audience: agents and maintainers.** Normal users describe the goal to Hermes in chat; the commands below are the backend surface that lets Hermes (or an operator) discover and configure the Maestro external-handoff stack for Claude Code and Codex. Discovery and configuration here are read-only and never spend a token; the one exception is the readiness probe in step 2, which runs a bounded no-op call against the CLI itself to earn the word "observed." Order follows `EXTERNAL_CLI_PROFILES` in `src/coding/executors.py`: claude-code first, then codex — neither is the implicit default. 1. **Install and authenticate the CLI yourself.** OMH does not install, log in, or hold credentials for either CLI — that stays each CLI's own installer and login flow, run once outside OMH. 2. **Verify with a readiness probe, not a file check.** ```sh omh coding executor-readiness --executor claude-code omh coding executor-readiness --executor codex ``` This is the honest evidence boundary the router itself holds (`READINESS_EVIDENCE_RULE`): *"A binary on PATH and an auth file are not run evidence."* A binary on `PATH` plus a local auth/config marker is `prepared`, not `observed` — the probe actually invokes the CLI (a `--version` or no-op call) and reads its configured model before calling it ready. Treat a `prepared`-only result as not yet ready. 3. **Optional: route per work category.** The Maestro lane resolves each unit's model from a category → model table (`ultrabrain`, `deep`, `quick`, `writing`, ...). Override it per profile with `omh coding category-maestro set codex ultrabrain gpt-5.6-sol:xhigh`, walk it guided with `omh coding category-maestro interview` (the interactive `omh setup` offers this walk too), and inspect the effective table with `omh coding category-maestro show`. A unit declares its category (`omh coding run --category `, or a `category` field on a fanout unit); an explicit `--model` always wins. See `docs/FANOUT.md` (Category-maestro). 4. **Optional: set a dispatch-model preference.** `omh coding fanout dispatch` spawns each CLI headlessly and falls back to a `--model` value only when a unit's prepared handoff routed no model at all. That fallback lives at `~/.omh/routing/dispatch-models.json` — seeded empty only by an explicit "yes" to the interactive setup's maestro question, otherwise operator-edited — (`omh_dispatch_model_preferences/v1`, a `profiles` map from owner to model string, e.g. `{"schema_version": "omh_dispatch_model_preferences/v1", "profiles": {"claude-code": "opus"}}`). Neither profile ships a shipped default; `"opus"` is documented here only as the recommended claude-code value for an operator whose account is entitled to that tier. A one-off run can skip this file entirely with `omh coding run --model `, which always outranks it. See `docs/FANOUT.md` (Dispatch-model preference) for the full schema. 5. **Check what the CLI's own skills contribute to a handoff prompt.** ```sh omh coding executor-skills --profile claude-code omh coding executor-skills --profile codex ``` Read-only and metadata-only: it reports the profile's locally *declared* skills (name, invocation string, role) that the Maestro lane arranges into a composed prompt — a discovered `SKILL.md` is evidence the file exists, never evidence the receiving agent loads or honors it. 6. **Know where routing sends the delegation intent.** Once a coding-owner choice for a run is explicit, the handoff is composed by the `ulw-maestro` skill — the skill-facing surface of the Maestro lane (`src/coding/maestro/`) described under "Hermes-native and Maestro ownership" above. `omh coding capability-snapshot` freezes the per-owner evidence a fanout dispatch requires before it will run, and `omh coding fanout dispatch` remains the one sanctioned execution surface: explicit per invocation, local-only, and it never merges (see Fanout: Parallel Split, Dispatch Bridge, and Merge Contract in `docs/FANOUT.md`). `omh_delegate_route`'s `~/.omh/routing/route-provenance.json` records only prepared routes, never dispatch or execution evidence. ## Windows OMH runs natively on Windows. The full test suite is an enforcing CI gate on `windows-latest`, not a smoke subset, so the library itself is held to the same standard as macOS and Linux. What follows is the install path, the config-home answer, and the capability boundary. ### Install ```powershell irm https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.ps1 | iex omh setup omh doctor ``` `install.ps1` is the PowerShell counterpart of `install.sh`. It reads the same `OMH_*` environment contract, resolves the package source the same way, and hands `omh setup` the same arguments. It requires Windows PowerShell 5.1 (shipped with Windows 10 and 11) or newer. If you would rather not pipe a remote script into `iex`, the manual path is equivalent — this is what the installer automates: ```powershell py -m venv $env:LOCALAPPDATA\omh\venv & $env:LOCALAPPDATA\omh\venv\Scripts\python.exe -m pip install --upgrade ` https://github.com/rlaope/oh-my-hermes/releases/download/v/oh_my_hermes--py3-none-any.whl & $env:LOCALAPPDATA\omh\venv\Scripts\omh.exe setup ``` The installer resolves `` for you from the `releases/latest` redirect. Doing it by hand means naming the release you want. Where it differs from `install.sh`, it differs because the platform does: | Behavior | POSIX | Windows | | --- | --- | --- | | Default venv | `~/.local/share/omh/venv` | `%LOCALAPPDATA%\omh\venv` | | Default command dir | `~/.local/bin` | `%LOCALAPPDATA%\omh\bin` | | How `omh` is exposed | symlink | `omh.cmd` shim (a symlink needs Developer Mode or elevation) | | PATH | hint printed | appended to the user PATH; set `OMH_ADD_TO_PATH=0` for hint-only | `OMH_ADD_TO_PATH` is the one option `install.ps1` adds. On POSIX, `~/.local/bin` is a convention most shells already carry on `PATH`, so `install.sh` only prints a hint. Windows has no equivalent convention, so a hint-only installer would leave every user with a command they cannot run. The change is user-scope, additive, announced in the installer output, and reversible. Installer step labels are English on Windows even when `OMH_LANG` is set. `OMH_LANG` is still validated and still forwarded to `omh setup` as `--language`, so the localized surface that carries real content stays localized. `install.ps1` is kept pure ASCII because Windows PowerShell 5.1 decodes a BOM-less script as the system ANSI code page and would render localized labels as mojibake. ### Which config home OMH targets `~/.hermes` and `~/.omh` are expanded by Python, and Python's `ntpath.expanduser` resolves `~` from `%USERPROFILE%` — it **ignores `HOME`** on native Windows: | Environment | Hermes home | OMH home | | --- | --- | --- | | Native Windows | `C:\Users\\.hermes` | `C:\Users\\.omh` | | WSL | `/home//.hermes` | `/home//.omh` | These are two separate stores on two separate filesystems. Installing under WSL does not give native Windows Hermes an OMH pack, and vice versa. Setting `HOME` in a PowerShell profile — a common carryover habit from WSL — has no effect on where OMH looks; use `HERMES_HOME` and `OMH_HOME` to override, which are honored identically on every platform. To see which store a given shell is actually talking to, `omh doctor` prints the config file it checked as `hermes_config: \config.yaml`. Operators who want the home on its own line can use the agent-facing `omh probe`, which reports `Hermes home:` directly. ### POSIX-only surfaces No skill is POSIX-only. Every skill in the catalog is guidance plus `omh` commands, which behave identically in PowerShell and in `sh`. What is POSIX-only is a set of storage and locking primitives. These surfaces **fail closed** — they refuse rather than weaken their guarantee — because they exist to make a safety claim that Windows cannot back: | Surface | Requires | On Windows | | --- | --- | --- | | Domain intelligence store (`omh memory domain-status`, `domain-capture`, …) | `O_NOFOLLOW`, `O_DIRECTORY`, dirfd opens, `fcntl` locks | Refuses with an explicit error | | Domain context attachment in `omh chat route` | same | Routing works; the expert question is not attached | | Prompt compatibility audit (`omh ops prompt-compatibility-audit`) | dirfd-anchored traversal | Refuses to read prompt sources | | Plugin static risk audit (`omh ops plugin-risk-audit`) | dirfd-anchored traversal | Refuses to scan plugin source | | Cross-harness benchmark sandbox | Linux `bwrap` process confinement | Reports `unsupported`; no real runs | | `0600` / `0700` artifact permissions | POSIX mode bits | `chmod` is close to a no-op on NTFS; private artifacts are not enforced private | | macOS menu bar helper | Darwin + `swiftc` | Skipped, and says so | Generic record locking is **not** on that list: `local_store` uses `msvcrt.locking` on Windows, so shared-record updates get a real OS lock with the same guarantees as POSIX. See [Architecture](ARCHITECTURE.md) for the locking model. ## What Setup Changes OMH's setup footprint is intentionally bounded: - It installs managed Hermes-visible skills and records local status contracts. - It can repair or reapply managed `skills.external_dirs` when a Hermes profile drifts. - It applies the same managed registration to every Hermes bot profile — each an independent home under `~/.hermes/profiles/` — so Desktop bot chats see the same OMH skills as the default chat. See [Bot Profiles](#bot-profiles). - It enables the managed `omh` plugin and selects the OMH memory provider only when the corresponding provider slot is free. Existing foreign ownership is preserved. - It defaults `display.interface: tui` whenever the user has not chosen an interface — on fresh configs and on existing configs alike, so upgraders reach the installed HUD without knowing about `hermes --tui`. - Interactive `omh setup` and `omh update` offer a default-Yes branded-TUI choice when the canonical config is not already `display.interface: tui` plus `display.skin: omh`. Accepting it (or passing `--yes`) sets both values, so bare `omh` and `hermes` open the same OH-MY-HERMES TUI. An already-active update does not ask. No or `--no-omh-tui` preserves the current values. JSON suppresses prompting; explicit canonical values remain unchanged unless `--yes` supplies consent. Dry-run never persists a previewed change. Noncanonical/quoted YAML shapes never prompt or change, even with `--yes`. Uninstall does not remove an accepted display selection. - It adds `auxiliary.compression.fallback_chain` when the config pins compression to a single provider and already lists other fallback providers. Without a compression fallback, one unreachable endpoint leaves a session unable to compress and unable to fall back — an unrecoverable `Cannot compress further` loop. The chain is derived only from providers the user already configured; an existing user-authored `fallback_chain` is never overwritten, and no endpoint is invented. See `examples/hermes-config.yaml`. - It keeps CLI output available for setup, doctor, update, and wrapper backends. - It does not patch Hermes core, run hidden coding work, or turn a prepared handoff into observed execution. The top-level `changed` value in `omh setup --json` is an aggregate: it is true when any managed setup field changes, including skill registration, compression fallbacks, plugin enablement, the fresh-config TUI default, or memory-provider selection. Model-alias writes remain a separate preview-and-approval step. ### TUI Themes OMH ships four palettes for the branded TUI. `omh setup` and `omh update` install all four into `$HERMES_HOME/skins/`, so switching is instant and offline — nothing is fetched when a theme is selected. | Theme | Skin name | Look | | --- | --- | --- | | `sky` | `omh` | Sky turquoise on deep teal. The default. | | `amber` | `omh-amber` | Amber gold on deep bronze. | | `crimson` | `omh-crimson` | Ember red-orange on deep crimson. | | `mono` | `omh-mono` | Neutral grayscale with white accents. | ```sh omh theme # arrow-key picker with a live colour preview omh theme list # plain listing, always (the scriptable surface) omh theme use crimson # select one directly (also accepts omh-crimson) omh theme use crimson --dry-run omh theme status # active skin, ownership, managed files on disk omh theme repair # report unmanaged theme files; writes nothing omh theme repair sky # adopt one back under OMH management omh theme repair --all --dry-run ``` Bare `omh theme` opens a picker: up/down arrows (or `j`/`k`) move the cursor, each theme paints a sample of its own palette as you pass over it, Enter applies the highlighted theme, and `q`, Escape, or Ctrl-C cancels without writing anything. It needs a terminal on both ends — with `--json`, a pipe, `TERM=dumb`, `OMH_NO_TUI=1`, or on Windows (no `termios`), bare `omh theme` prints the same plain listing `omh theme list` prints. `NO_COLOR` keeps the picker but drops every escape sequence, naming the hex values as text instead. Rules worth knowing: - **Selection is one config key.** `omh theme use` writes `display.skin` in the Hermes config and nothing else. It never patches Hermes. - **A theme applies on the next Hermes start.** `omh` runs `hermes` as a child process and Hermes reads its skin at startup, so a running session keeps the look it opened with. Restart it. - **An explicit choice survives updates.** `omh theme use` is the consent, and every later `omh setup` / `omh update` leaves the chosen theme alone — the default is written only when `display.skin` is unset. A foreign skin (say `ares`) is preserved the same way and reported as yours, not replaced. - **A hand-edited theme file is yours.** Each of the four skin YAMLs is tracked in its own manifest record, so editing `omh-mono.yaml` keeps that file untouched forever without stopping the other three from being refreshed. `omh theme status` reports such a file as `unmanaged`. - **A stale manifest heals itself.** OMH owns a theme file when the manifest record matches it *or* when its bytes are identical to the currently shipped template. The second proof exists because a manifest can fall behind the file it describes — an older update refreshed a skin without refreshing its record — and record-only ownership would then read OMH's own file as hand-edited and stop updating it forever. Adopting our own bytes cannot destroy anything you wrote, because overwriting them is a no-op. A file matching neither proof stays `unmanaged`. #### Why a theme file can be `unmanaged` `omh theme status` reports each theme file as `managed`, `unmanaged`, or `missing`. `unmanaged` means OMH cannot prove it wrote that file, so it will never overwrite it — and therefore never update it either. Two very different situations end up there: 1. **You edited it, or wrote your own.** Working as designed. The file is yours, it keeps winning over every future release, and nothing needs fixing. 2. **It is OMH's file, stranded.** The manifest record went stale at some point *and* the shipped template has since changed, so the file now matches neither ownership proof. It is ours in origin but indistinguishable on disk from case 1 — and it is frozen on an old palette forever, because no later release can reach a file OMH will not touch. Nothing on disk separates those two cases, which is why OMH cannot fix case 2 on its own. `omh theme repair` resolves it by asking you: ```sh omh theme repair # report only; safe and idempotent, writes nothing omh theme repair sky # adopt sky: overwrite it with the shipped file and record it omh theme repair --all # adopt every unmanaged theme file omh theme repair sky --dry-run # show exactly what adopting would do, without doing it omh theme repair --json # machine-readable payload (omh_theme_repair/v1) ``` The bare form and `--dry-run` never write. Both print, per file, the before/after `sha256` and the palette tokens that would change, so you see what you are accepting before anything is destructive: ```text OMH theme repair Skins directory: /Users/you/.hermes/skins Theme files sky omh.yaml - unmanaged; NOT adopted (name it, or pass --all, to accept) sha256 e99be0e84830b659 -> 122577bf7c5080e3 ui_label: #7FDBFF -> #9FE8FF amber omh-amber.yaml - managed; untouched crimson omh-crimson.yaml - managed; untouched mono omh-mono.yaml - managed; untouched Next Nothing was written. Accept with `omh theme repair ` or `omh theme repair --all`. ``` Naming a theme (or passing `--all`) IS the consent — there is no hash that can give it, which is why the command asks for a name instead. Rules: - **Nothing repairs automatically.** `omh setup`, `omh update`, and `omh theme use` never call the repair path. A skin you wrote is never silently overwritten. - **Already-`managed` files are untouched** and reported as such, whether or not you named them. - **A `missing` file is installed when named**, matching what `omh setup` and `omh update` already do for a theme file that is not there. - **Repair is reversible the usual way.** A repaired file is a managed file, so `omh uninstall --all` takes it away exactly like an installed one. - **After a repair, updates flow again.** The file is recorded in the manifest, so the next release's template change lands on it normally. ## Bot Profiles Hermes bot profiles (`hermes profile create`, Desktop bot chats) are fully independent Hermes homes under `~/.hermes/profiles/` — each with its own `config.yaml`, skills resolution, and plugin directory. A registration written only to the primary home never reaches them, which is why a bot chat can show zero OMH skills while the default chat has the full set. `omh setup` and `omh update` sync every profile automatically: - an already-registered profile is refreshed to the running version — its plugin bundle, TUI widget, and skin, under the same manifest-checked refusals the primary home gets; - a profile with no OMH bundle at all — including a bot created after install — gets the full bootstrap on the next `omh setup` or `omh update`; - a deliberately unregistered profile (see below) is left alone. "Registered" means the profile names any OMH-managed skills directory, not the exact one this install would write today. A profile registered at `~/.omh/skills` before the command install moved to its shared generation pointer is still registered: it gets refreshed and carried forward, not read as an opt-out and frozen on the generation it was installed at. Only a profile naming none of them has opted out. `omh uninstall` is symmetric with the sync. A full uninstall (`omh uninstall`, `--all`, or `--purge`) clears every profile's registration and removes its managed artifacts — the plugin bundle, the TUI widget, and the skin — through the same manifest checks the primary home gets: a profile directory OMH cannot prove it owns is kept and reported, never deleted blind. `--registration-only` removes every OMH-managed entry from each profile — the same directories the sync reads, so nothing is left behind for the next update to score as still registered — while keeping their plugin directories, which is exactly the deliberate opt-out state described below. After a sync, restart Hermes Desktop so bot chats reload their skills. To keep OMH out of one bot, unregister that profile only: ```sh omh --hermes-home ~/.hermes/profiles/ uninstall --registration-only ``` That removes every OMH-managed skills directory the profile's config named, whichever one it was registered at. The plugin directory stays in place as the opt-out marker; setup and update never re-register a profile in that state. OMH workflows are skill triggers, not Hermes slash commands, so they do not appear in the `/` autocomplete — in any chat, bot or default. Invoke them as `$ulw …`, `$plan …`, `$research …` (or plain phrasings like `ulw work …`), and list what is installed with `/skills`. The one `/` entry OMH adds is `/omh-model`, the per-category model-chain picker the Modern-TUI widget registers as a modal app; it exists only in the Modern TUI, where the widget loads. ![/omh-model in the Modern TUI: the same rows and keys as the CLI picker, over the transcript](../assets/omh-model-tui.png) The curl installer intentionally stops before setup. It installs the isolated command package and `omh` executable only. `omh setup` is the explicit, repairable step that installs generated managed skills and registers them with Hermes through `skills.external_dirs`. When `omh setup` is run in a real terminal, it asks exactly one question — install scope (user or project). Output is English by default (`--language` or `OMH_LANG` opt into ko/ja/zh), Hermes registration defaults to on (`--skip-apply` opts out), and there is no upfront coding-agent question: Hermes asks who should own coding work at the first coding request, in natural language. Optional surfaces stay behind flags — `--with-mcp` for the tool bridge, `--with-menubar`/`--no-menubar` for the menu bar, `--star` to star the GitHub repo. Team/profile packs and operating models stay available as explicit commands or flags, but setup does not make a user lock the whole organization shape during first install. In non-interactive shells it uses the same safe defaults and prints a concise step-by-step summary. Use `omh setup --json` or `OMH_OUTPUT=json omh setup` for the full machine-readable payload. Setup also records OMH project-memory policy in `.omh/setup-profile.json`. The default is safe `review-first` memory: OMH can capture local candidates, but reviewed records are required before recall enters coding handoffs. Operators can choose: ```sh omh setup --memory-mode off omh setup --memory-mode review-first omh setup --memory-mode auto-safe ``` `auto-safe` approves only locally safe summaries and leaves credentials, raw logs, full transcripts, short-lived PR/commit IDs, and temporary task progress for review or rejection. This is OMH project memory under `.omh/memory/`; setup does not mutate Hermes global or internal memory. The default user scope writes `~/.omh` and `~/.hermes`. Use project scope when one repository needs isolated local OMH skills and Hermes config: ```sh omh setup --scope project omh --scope project doctor ``` The installer also prints the installed `omh` command path. By default it uses an isolated OMH virtual environment and links `omh` into a user bin directory when possible. If that directory is not on `PATH`, add the printed directory to `PATH` or run the printed absolute `omh` path directly. `omh doctor` includes a non-blocking command availability warning for this case, so source checkouts, wrapper runtimes, and absolute-path installs can still verify Hermes registration without pretending the shell alias is ready. ### Build identity `omh --version` and `omh doctor` both name the code behind the command that actually ran, because the semantic version alone cannot tell two same-version checkouts apart: ```text omh 2.0.1 (source 6da2a3ca, clean) omh 2.0.1 (editable 6da2a3ca, dirty) omh 2.0.1 (build identity unavailable: installed package, no stamped identity) ``` `omh doctor` prints the same line, and `omh doctor --json` carries the full `build_identity/v1` block: semantic version, install kind (`source_checkout`, `editable_install`, `installed_package`, `standalone_artifact`, `unknown`), the resolved command path, identity status, identity source, the full commit SHA, dirty state, a reason when identity is unavailable, and a claim boundary. Read the fields with these boundaries in mind: - The revision is resolved from the running package's own source location, not from the caller's working directory, and only from a repository whose `pyproject.toml` declares this project. A wheel installed into a virtual environment inside somebody else's checkout therefore reports `unavailable` instead of borrowing that project's revision. - Only identity is serialized. No branch name, remote URL, diff, changed-file list, or environment data appears in either output. - Dirty state is `unknown`, never guessed, when the revision came from reading `.git` directly because no `git` binary was available. - Published OMH artifacts carry no stamped identity today, so a wheel, npm, or Homebrew install reports `unavailable` with `no_stamped_identity`. That is the explicit compatibility result: a packager that ships a `build_identity_stamp/v1` `_build_identity.json` inside the package is read, and an artifact without one is never given an invented revision. - The block is diagnostic provenance only. It is not evidence that the named revision was tested, reviewed, passed CI, was published, or behaved correctly at runtime, and it never changes the doctor status or exit code. Plugin support is installed by `omh setup` by default. It provides a thin Hermes plugin bridge in addition to the skill pack: That installs `~/.hermes/plugins/omh` with deterministic workflow recommendation, metadata-only HUD/status/role support, and a bounded evidence probe. `omh hud` prints the same compact status line a Hermes TUI or plugin surface can render. It shows only operationally useful status: OMH version, plugin readiness, target topology, the coding-agent segment described below, and evidence state. Skill counts, setup inventory, token metadata, and deep diagnostics are left to `omh doctor`, `omh_status`, and machine-readable HUD JSON. The HUD payload also carries a metadata-only plan todo list. When a todo is declared — by Hermes through the `omh_todo` plugin tool, or by an agent or operator through `omh runtime todo set` — the modern Hermes TUI (`hermes --tui`) renders it as a compact checklist above the prompt input, with the status and activity rows below the input. The classic Python TUI does not load TUI widget files, so this panel is a modern-TUI-only surface. `omh runtime todo show` prints the todo projection the HUD payload carries (`todo` plus `display.todo_lines`), and `omh runtime todo clear` removes it. Todo items are plan declarations, never execution, review, CI, or merge evidence; an all-done list collapses to a single header line and a list untouched for 24 hours is hidden as stale. The panel belongs to the session that declared the plan. When Hermes declares a plan through `omh_todo`, the record is stored for that session (`$OMH_HOME/runtime/todos/.json`) and read back only for it: the modern TUI widget names its own session from the host's active-session file on every poll, and the plugin tools (`omh_todo`, `omh_hud`) and the pre-LLM reminder name the session Hermes dispatched them for — never one named in tool arguments, so a model cannot address another session's record. A plan declared from a Slack or Discord gateway session, or from a second TUI open at the same time, is its own record, so it neither renders in another session's panel nor overwrites that session's checklist. Per-session records are pruned on write once they pass the 24-hour stale bound; nothing else in that directory is touched. `omh runtime todo set|clear|show --session ` addresses one session's record from the command line. Records written without a session id — `omh runtime todo set` with no `--session`, or anything predating the field — are the home-wide `$OMH_HOME/runtime/todo.json`, scoped by write time instead: a plan written before the reading session started belongs to an earlier one and reads as stale. That fallback only applies where the host can answer it. With no `$HERMES_HOME/state.db`, an unreadable one, no live TUI session recorded in it, or a reading session it does not list as a TUI (a gateway session has no row to date the record against), the projection keeps the age-only behavior above and shows the plan, since hiding a legitimately current checklist on missing evidence is the worse failure. The widget's identity likewise needs a host that sets `HERMES_TUI_ACTIVE_SESSION_FILE` for the TUI process; on a Hermes that does not, the widget carries no identity and the panel answers for the most recently active live TUI session, as it did before. The widget's own identity has one known alias. After a resume or session switch the host's active-session file holds the durable session key; on a freshly created session it holds the gateway's transport id, which no record and no `state.db` row carries. The reader treats a widget reference that names no live TUI row and owns no record as that case and answers as an identity-less poll would — the most recently active live TUI session — so a fresh TUI still renders the plan it declares. Two TUIs both freshly created and not yet resumed therefore still share that answer — the pre-existing most-recently-active rule — until each is resumed or switched and the file carries its durable key; the plugin tools and the reminder are unaffected because Hermes dispatches them with the durable key. Native agent activity is a separate ownership policy from todos. A mapped valid durable identity selects native `state.db` children before row limits and totals, following only unambiguous compression continuations, never branches or delegated children. Missing, invalid or unmapped identities retain the previous global native fallback, including best-effort manifest context. Rows and aggregate scope explicitly say `global`; the widget renders those labels instead of attributing them to this chat. Mapped native rows omit unowned manifest/route labels. OMH executor, Maestro and DAG activity remains available in the widget, explicitly global; mixed totals say `this chat + global`. This changes display attribution only, not delegation or the todo policy above. #### Status model: no-run, prepared-handoff, observed-run `omh setup` deliberately records a safety-first `choose` preference and asks no upfront coding-owner question, so Hermes asks which coding agent to use at the first coding request instead of at install time. The HUD line and the coding metadata retained by the `menubar_status/v2` payload follow the same three-state model so that an unselected coding agent never reads as an idle external agent named `choose`/`ask`: 1. **No-run.** No coding request has been routed yet. - No preference recorded (the normal safety-first default): the HUD `coding-agent` segment is executor-neutral, `coding-agent:not-selected`, and the menu bar payload's `settings.coding_handoff` reads `Coding agent: Not selected` with `source: "none"`. The menu ends with a compact `coding` metadata footer rather than presenting this as an observed run. - A real preference was recorded (for example `omh setup --default-executor codex`): the executor name is shown because it is a genuine user choice, not a placeholder — `coding-agent:idle(codex)` on the HUD line, and `Coding agent: Codex` with `source: "user_preference"` in the menu bar payload. 2. **Prepared handoff.** `omh coding delegate --record` prepared a handoff for a run but execution has not been observed: `coding-agent:prepared(codex)` on the HUD line, and the menu bar payload records `source: "prepared_handoff"`. 3. **Observed run.** A run recorded observed evidence (dispatch, execution, verification, review, CI, or merge): the HUD line shows the run's actual phase, for example `coding-agent:runtime(codex)`, and the menu bar payload records `source: "observed_runtime"`. The `evidence` HUD segment keeps the same prepared-versus-observed boundary as before. A quiet no-run line looks like `[omh] v1.0.6 | plugin:ready | target:single | coding-agent:not-selected`. That version is the **installed** version, never the newest published release: it is what `omh --version` prints, and for the TUI widget it is the version `omh install`/`omh update` recorded, because the widget runs the plugin-bundle reader without being able to import the `omh` package. A machine that has not run `omh update` since a release keeps showing the version it installed, and an already-open terminal keeps showing it until the TUI restarts. See [Release](RELEASE.md), "After the Cut". The plugin also exposes `omh_context` for a compact OMH mental model plus generic-tool checkpoint, `omh_memory` for a metadata-only comparison of Hermes memory against OMH's approved records, `omh_interact` for shell-free chat responses and metadata-only wrapper session records, `omh_recommend` for route hints without session recording, `omh_probe` for local setup/runtime status and capability-roadmap cards, `omh_role`, validates `[omh-role:name]` markers for delegated subagent prompts, and records a metadata-only session-end checkpoint when OMH runtime state exists. It also exposes `omh_gather_evidence` for explicit allowlisted local verification probes such as OMH doctor, harness validation, docs checks, unittest, compileall, and whitespace checks. It does not provide an arbitrary shell, patch Hermes core, dispatch executors, prove execution, or prove Hermes has loaded it. Wrapper session records include `record_provenance` so plugin-authored metadata and wrapper/backend metadata remain distinguishable. If the target Hermes runtime requires a separate plugin enable command, follow that runtime's plugin enable/reload step. For a quick terminal check of the native menu bar/status-widget surface, use the human-readable summary: ```sh omh menubar status ``` It prints Summary, Sessions, Models, the compact coding metadata footer, and Observation sections instead of a raw JSON blob. For native menu bar, status-widget, wrapper, or automation integrations, use the platform-neutral view model: ```sh omh menubar status --json ``` The `menubar_status/v2` JSON retains the separate `hermes_agents` and `external_coding_executors` metadata and adds read-only Hermes process, session, and model observations. Its `display.menu_cards` contains Sessions and Models tables followed by one compact `coding` metadata footer. The Sessions columns are exactly `Hermes session` / `Count`, and its rows are only `live` and `total`; source or TUI breakdown is intentionally not shown. Session counts come from a read-only read of Hermes' own session store. In Models, `current` is the model observed on the live Hermes session, while `main` and any auxiliary alias rows are settings read from Hermes configuration. A configured model is not evidence that a request used it. The `settings.coding_handoff.source` field continues to distinguish `"none"`, `"user_preference"`, `"prepared_handoff"`, and `"observed_runtime"` per the status model above. On macOS, a normal user-scope `omh setup` also attempts to build and start the small OMH menu bar helper when `swiftc` is available. The helper lives under `~/.omh/menubar`, is started with a user LaunchAgent, and refreshes the same `omh menubar status --observe-local-processes --json` payload. The visible menu is grouped as Sessions and Models tables with a compact coding metadata footer instead of a raw text list. The helper explicitly requests the bounded local process scan so its header can show observed Hermes agent/process counts; plain `omh menubar status` does not scan processes unless `--observe-local-processes` is supplied. Use explicit commands when you want to manage it yourself: ```sh omh menubar install omh menubar start omh menubar stop omh menubar uninstall ``` Set `OMH_MENUBAR=0` or run `omh setup --no-menubar` to skip the helper. Run `omh setup --with-menubar` to request it explicitly. Missing `swiftc` or a failed helper start does not make the OMH workflow setup fail; setup reports the menu bar step separately. A native macOS MenuBarExtra app, the OMH menu bar helper, or a test harness can pass a short-lived `menubar_process_overlay/v1` file, or ask the backend to do a bounded local process observation, when it has actually observed local process state: ```sh omh menubar status --overlay /path/to/overlay.json omh menubar status --observe-local-processes ``` The overlay and local observation are app-local and expire by TTL. OMH does not infer that a prepared coding-agent action was executed, reviewed, passed CI, or merged. The session-store and configuration observers are local and read-only: the status path makes no network request and does not write Hermes-owned files. MCP bridge setup is also optional and intentionally conservative: ```sh omh setup --with-mcp omh setup --with-mcp --mcp-host codex omh setup --with-mcp --mcp-host claude-code omh mcp manifest omh mcp config-recipe --host claude-code omh mcp config-recipe --host codex omh mcp config-recipe --host opencode omh mcp config-recipe --host cursor # wrapper/host adapters can record observed host load when they see it: omh mcp observe-host --host hermes-agent --session --event host_load --evidence-ref ``` `omh setup --with-mcp` records `mcp_mode: bridge_requested` in setup state and keeps `observed: false` until a Hermes/MCP host records a concrete load or tool-call event. Add `--mcp-host codex`, `--mcp-host claude-code`, `--mcp-host opencode`, or `--mcp-host cursor` when you want setup to write the local host config entry for the OMH stdio server. Use `--mcp-config-path` when the host config lives somewhere non-standard, and `--mcp-command` when the host needs an absolute installed `omh` command path. `omh mcp manifest` prints the generic stdio MCP bridge contract, and `omh mcp config-recipe --host ...` prints host-shaped copy-paste snippets for common MCP-capable environments. Config text written by setup or printed by a recipe is still only host-config evidence, not host-runtime evidence. The bridge exposes only local `omh_status`, `omh_recommend`, and `omh_probe` tools; it is not arbitrary shell access, connector execution, coding dispatch, or proof that an MCP runtime is active. `omh mcp observe-host` is for host/wrapper adapters that already observed bridge load or use and can attach a stable evidence reference. It records `omh_mcp_host_session/v1` metadata; it does not discover or force host loading. The OMH plugin follows the same evidence split. `omh setup` installs the plugin bundle and `omh doctor` can prove local import/register smoke. A Hermes host or wrapper that actually sees the plugin load can record that runtime event: ```sh omh plugin observe-host --host hermes-agent --session --event plugin_load --evidence-ref omh plugin observations ``` This writes `omh_plugin_host_observation/v1`. It is plugin load/use evidence only; it is not coding dispatch, implementation, review, CI, merge, or proof of unrecorded plugin calls. Observed `plugin_load`, `tool_call`, `hook_call`, or `status_query` records count as active runtime observations. Observed `session_end` or `plugin_unload` records are historical runtime evidence only. `blocked` means the host or wrapper could not inspect the plugin state; it does not preserve an older active-ready claim. When the managed plugin is actually invoked, hosts can also pass bounded `observation` metadata to OMH plugin tools/hooks. The plugin then records the same `omh_plugin_host_observation/v1` event automatically, without storing raw prompts or tool bodies. This proves only the recorded plugin tool/hook use. ## Install Path A: Hermes-Native Skill Tap Use this path when the target Hermes environment supports skill taps: ```sh hermes skills tap add rlaope/oh-my-hermes hermes skills install rlaope/oh-my-hermes/skills/omh-routing --yes ``` Use the full identifier for first install. It avoids short-name resolver ambiguity in current Hermes CLI releases while installing the same `oh-my-hermes` skill. Install additional workflow skills when you want direct Hermes skill surfaces: ```sh hermes skills install ulw-interview hermes skills install ulw-plan hermes skills install ulw-research hermes skills install omh-code-review ``` This path reads the tap-compatible skill pack under `skills/` in this repository. After installation, restart or refresh Hermes Agent if the target environment requires it, then use Hermes normally: ```text Use OMH request-to-handoff for: I want to safely add a feature to this repo. ``` Hermes should route through the installed skill guidance, name the responsible role, and show the next action without asking the chat user to run `omh` commands. ## Hermes CLI Release Smoke For release candidates, OMH provides a dedicated smoke contract for the real Hermes CLI install path. The default command is a plan-only check that can run in CI without touching the current Hermes profile: ```sh omh release hermes-smoke ``` The installer path has a separate first-time downloader smoke. Plan mode is also safe for CI and only describes the isolated HOME, venv, bin directory, command-install and installed-command checks: ```sh omh release install-smoke ``` When you want observed evidence that `install.sh` itself works from a checkout, run it live. This still does not mutate your real Hermes profile; OMH creates a temporary HOME, virtual environment, and bin directory, then runs `install.sh` and installed-command smoke inside that isolated target. It does not run setup or doctor unless an advanced one-shot compatibility smoke opts in with `--run-setup`: ```sh omh release install-smoke --live --repo-root "$PWD" --install-script "$PWD/install.sh" ``` The plan also reports `installed_command_smoke` and `first_use_status_smoke`. The first checks that the installed `omh` command is discoverable on PATH before proving the console script can run; the second locks the first Hermes chat/status boundary so pre-handoff status does not show executor open/result actions. After installing OMH into the target runtime, verify the command path too: ```sh command -v omh omh --help omh release skill-content-smoke --json omh release product-readiness --version 1.0.5 --json omh release evidence-bundle --version 1.0.5 --write --repo-root "$PWD" --json omh release evidence-bundle --version 1.0.5 --verify --repo-root "$PWD" --json omh --omh-home /tmp/omh-smoke --hermes-home /tmp/hermes-smoke release hermes-smoke --install-path setup --omh-command omh --include-command-smoke ``` `release skill-content-smoke` checks the installed command package's generated skill guidance, including router awareness and workflow context rails. It also checks bundled role context, all-skill awareness lane coverage, full capability manifest context, playbook capability context, standalone plugin capability fallback coverage, fallback routing/context/boundary fields, bounded prompt context budgets, and bounded capability payload budgets. In short, it preserves bounded context budgets while still giving Hermes enough OMH workflow context to route well. It is not Hermes chat-load evidence. When an operator explicitly wants live evidence from the target Hermes profile, run one of these: Use `omh release product-readiness --version 1.0.5 --json` when you want a single release-candidate card that combines skill content, G1-G10 use-case readiness, parity contracts, and release checklist shape. It is still local contract evidence, not live Hermes chat or executor evidence. Use the evidence bundle when you want that local release-candidate evidence written under `.omh/runtime/release-evidence/` for a release PR or release note: ```sh omh release evidence-bundle --version 1.0.5 --write --repo-root "$PWD" --json ``` The bundle is not CI, live Hermes smoke, executor, delivery, merge, or GitHub release evidence. #### Revision-bound evidence and `--verify` Maintainer and release-automation surface. Nobody installing or using OMH needs to run any of it. The bundle used to record what the gates said without recording which source they said it about. Attached to a release, that reads as provenance for whatever the reader assumes. It now binds to a revision, and the binding can be re-checked without regenerating anything. `--repo-root` names the checkout being described. OMH reads it only, never writes to it, and records the full commit hash, the full tree hash, whether the worktree was dirty, and a deterministic input manifest of repo-relative paths and `sha256:` digests. Absolute paths never land in the file, and the recorded artifact is stored by basename. Re-check a recorded bundle with: ```sh omh release evidence-bundle --version 1.0.5 --verify --repo-root "$PWD" --json ``` Verification is pure. It never writes and never regenerates, so a run that disagrees with the bundle cannot silently repair it. The verdict is one of a closed set, and exit code 0 means `matching` and nothing else: | Verdict | Meaning | | --- | --- | | `matching` | Same commit, same tree, digests agree. The only exit-0 verdict | | `dirty` | The worktree has uncommitted changes, so no clean claim is possible. Checked first and it wins over `stale` | | `mismatched_revision` | The recorded commit is not the one you are on now | | `stale` | Same commit, but the tree, a declared input digest, or the artifact digest moved | | `unverifiable` | Identity is unavailable on one side, so the answer is unknown rather than assumed | | `legacy_schema` | A pre-v2 bundle. It can never read as `matching`, because it was never bound to a revision | | `missing` | No bundle is recorded for that version | `--write` is fail-closed the same way. Without a usable identity, meaning no git checkout and no explicit `--archive-digest` or `--artifact-digest`, the bundle is still written for local use but reports `status: needs_attention` and `publication_ready: false`, and the command exits non-zero. It never quietly passes. Compatibility: this is a deliberate hard bump to `omh_release_evidence_bundle/v2`. Bundles already on disk stay exactly as they are and verify as `legacy_schema`; that is the point, since a v1 file genuinely was not revision-bound. The evidence index stays at v1 and only gains `commit_sha` and `tree_sha` alongside what it already had. Release publication regenerates the bundle from the exact tagged checkout and refuses to publish when the binding does not hold; see [Release](RELEASE.md) and [Distribution](DISTRIBUTION.md) for the maintainer flow. What this proves is provenance for the recorded revision. It is still not CI, live Hermes smoke, executor, delivery, merge, or GitHub release evidence. ```sh omh release hermes-smoke --live --install-path tap --target-confirmed omh --omh-home /tmp/omh-smoke --hermes-home /tmp/hermes-smoke release hermes-smoke --live --install-path setup ``` The live smoke runs the selected install path and then verifies: ```sh hermes skills tap list hermes skills list --enabled-only hermes skills check oh-my-hermes hermes skills inspect rlaope/oh-my-hermes/skills/omh-routing ``` The tap path proves Hermes CLI install/list/check/inspect for the target profile. The setup path proves `skills.external_dirs` discovery with list/check plus `omh doctor`, because current Hermes CLI releases do not reliably inspect local external-dir skills by short name. Neither path proves that a later Hermes chat session selected OMH unless that chat response is observed separately. ## Install Path B: OMH Bootstrap Setup Use this path when you want a Python installer, generated managed skills, local doctor checks, or wrapper/backend operations in the same runtime context as a hosted Hermes wrapper. Run the installer: ```sh curl -fsSL https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.sh | sh ``` By default this installs the `stable` channel: the newest published release, as a wheel. To pin a specific release instead, pass its version — the channel is already the default, so naming it is optional but harmless: ```sh curl -fsSL https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.sh | OMH_CHANNEL=stable OMH_VERSION= sh ``` To track the unreleased `main` branch instead, ask for `preview` explicitly: ```sh curl -fsSL https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.sh | OMH_CHANNEL=preview sh ``` The two channels download different artifacts, and the difference is large enough to plan around: | Channel | Artifact | Measured size | Measured time | | --- | --- | --- | --- | | `stable` (default) | `oh_my_hermes--py3-none-any.whl` release asset | 2,714,885 bytes at v1.0.6 | 0.55s | | `preview` | `main` branch repository archive | 46,012,605 bytes on 2026-08-15 | 5.71s | Sizes and times measured 2026-08-15 with `curl`. Two things make the gap bigger than the byte ratio suggests. The preview archive is the whole repository, including `assets/`, `tests/`, and `site/`, none of which is needed to run `omh`. And GitHub *generates* `archive/refs/heads/.zip` on demand for every request rather than serving a cached object, so preview pays generation latency each time — on an ordinary connection that download has been observed to take over five minutes. Release assets are static objects served from a CDN. Preview stays an archive because GitHub publishes release assets per tag and there is no per-branch wheel to point at. Use it only when you specifically need unreleased `main`. A version-less `stable` install asks GitHub which release is newest by reading a single redirect (`releases/latest`), which costs about 0.25s. If that lookup fails, the installer says so and tells you to pass `OMH_VERSION` or switch to `OMH_CHANNEL=preview`; it never guesses a URL. The installer does this lookup itself and passes the resolved version to `omh setup`, because `omh` makes no network calls of its own. > **`omh update` still defaults to `preview`.** Only the installer default > moved. Until the release-version lookup has a home inside `omh` that does not > break its no-network boundary, a plain `omh update` keeps fetching the branch > archive. To get the slim path from `omh update` today, name the release: > `omh update --channel stable --version `. Releases published before the wheel-publishing workflow existed carry no asset. If a stable install reports a 404 for the wheel, install that tag from the repository archive instead: ```sh curl -fsSL https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.sh | OMH_PACKAGE_URL=https://github.com/rlaope/oh-my-hermes/archive/refs/tags/v.zip sh ``` For custom release archives or local package sources accepted by `pip`, pass `OMH_PACKAGE_URL`. To install from a fork or mirror, override `OMH_REPO_ASSET_ROOT` and `OMH_REPO_ARCHIVE_ROOT`. The installer creates an isolated OMH virtual environment and links the `omh` command into `~/.local/bin` when possible. It does not run `omh setup`, register Hermes skill directories, install plugin state, or run `omh doctor` by default. That avoids Homebrew and distro Python `externally-managed-environment` failures while keeping the setup boundary visible: install the command first, then run `omh setup` when you are ready to connect OMH to Hermes. Installer and setup output can be localized with `OMH_LANG` or `--language`. Supported language codes are `en`, `ko`, `ja`, and `zh`: ```sh curl -fsSL https://raw.githubusercontent.com/rlaope/oh-my-hermes/main/install.sh | OMH_LANG=ko sh omh setup --language ko ``` Installer localization is separate from routing localization. Backend routing surfaces such as `omh recommend`, `omh playbook recommend`, and `omh chat interact` use a deterministic local phrase layer for tested Japanese, Chinese, Korean, Spanish, French, German, and Hindi operator-routing requests. The layer expands known phrases into canonical routing signals, includes `locale::