Harnesses, models and effort
Three top-level sections say what a session can be run as: which harnesses this machine has, which models a work item may be put on, and how much effort it may be asked for.
harnesses:
- name: claude
default: true
defaultModel: opus-5
args: ["--dangerously-skip-permissions"]
- name: cursor
models:
- opus-5
- fable-5.1
- name: gpt-5.6-sol
harnesses: [cursor]
effort:
- low
- medium
- high
- xhigh
- max
- ultraThey sit at the top level, beside repositories and critics, rather than under routing. routing configures how an event reaches a session; these configure what a session is. A work item then picks a model and an effort level at the phase-selection gate, and the choice is frozen with the rest of that gate's answers (issue-358, decision-124).
Two rules carry the whole design
A declaration may narrow; only the probe may confirm. Nothing you write here asserts that a harness can run a model. the-loop models check asks the harness and caches the answer, and only an ok (or an unprobed unknown) makes a model offerable. What a declaration can do is restrict which combinations are offered and probed at all.
the-loop owns the effort vocabulary; the provider owns the model name. An effort level is a the-loop concept that harnesses spell differently, so there is one enum and each adapter translates it. A model name is the provider's identifier, so it is copied verbatim and never parsed for a vendor.
The declarations
The harnesses
The harnesses list declares the agent harnesses this instance has. Unset, the-loop falls back to routing.defaultHarness, which is what every install written before this key has.
The agent harnesses this instance has. models and effort refer to these names, and a work item's harness is what decides which models it may be offered.
Today an entry carries name, default, defaultModel and args. The other three per-harness keys — routing.harnessTrust, routing.harnessPlugins and routing.defaultHarness — belong here by exactly the same argument, and moving them is a named follow-up rather than a silent migration.
harnesses[].name
- Type:
string - Default: none — required
The harness's name as the-loop's adapters know it: claude or cursor. A name with no adapter is declared but unusable; the-loop models check reports that rather than letting a spawn discover it.
harnesses[].default
- Type:
boolean - Default:
false
Whether an unmatched event spawns on this harness. With no entry claiming it, the-loop falls back to routing.defaultHarness. Two entries claiming it is a config error — a work item's harness decides which models it may be offered, so it cannot be ambiguous.
harnesses[].defaultModel
- Type:
string - Default: unset — no model flag is passed
The model a work item's session on this harness is launched on when it has no model of its own (issue-451):
the-loop do,the-loop contributeandthe-loop review, which have nophase-selectiongate;- a
phase-selectionreply that ticked no model, or an install with nomodelsat all; - a frozen model that no longer passes re-validation (withdrawn from
models, or narrowed to another harness).
Unset, the-loop passes no model flag and the harness's own settings decide — an enterprise policy, the user's settings file, or the vendor's current default. Set it when you want those sessions on a model you picked.
harnesses[].args → --dangerously-skip-permissions
+ the default model → --model opus-5 (only when the work item chose none)A work item's own model always wins, and the default is never added beside it. The name follows the same grammar as a models name and is copied verbatim into the harness's model flag; a value outside the grammar is ignored. It does not need to be declared in models — the default exists for the install that offers no choice.
The default is the operator's answer, read at launch: it is not frozen into the work item at phase-selection, so changing it moves every work item that chose nothing. A session already running on other arguments is re-launched on its next event, resuming its conversation, the same as when you change args. The checklist and the confirmation name the default so the person answering the gate knows what an unticked section means.
the-loop models check probes the default on this harness. A default the harness refuses is dropped at launch, like a refused choice. A harness whose adapter has no model flag cannot apply one, and the-loop models list|check warns.
Standing sessions and critics do not read it: a standing session's own harnessArgs, and a critic's own model, say which model they run.
harnesses[].args
- Type:
string[] - Default:
[]
Extra CLI arguments every session on this harness is launched with. This is the new home of routing.harnessArgs.<harness>, which still works and warns.
Every session means every one: a work item's first session, the one spawned when its phase-selection gate is answered, a respawn, a pull request's own session, a standing session whose entry omits harnessArgs, and the argv the-loop models check probes with. One resolver produces the list and every adapter is built from it (issue-377 — until 19.2.0 only a session whose work item had frozen a model or an effort level read this key; every other session launched on routing.harnessArgs alone, which is empty in a config that followed the deprecation warning). A harness declared in both homes launches on this one — never on the union — and the-loop models list|check reports the conflict.
A work item's own model and effort are appended to this list, never merged into it:
harnesses[].args → --dangerously-skip-permissions
+ the model's args → --model fable-5.1
+ the effort's args → (whatever that harness's adapter produces)the-loop does not remove, rewrite or reorder an argument you declared, and never adds a permission flag you did not. If your list already carries the same flag a choice would append, the-loop models list|check warns — whether the appended one wins is the harness's own argument-parsing rule, not the-loop's to assert.
In interaction.mode: work-item (the default), a Claude Code session also gets --disallowedTools=AskUserQuestion, after everything above and immediately before the prompt (issue-426). You don't need to add it yourself.
Write a variadic flag with =. Claude Code's list-valued flags (--disallowedTools, --allowedTools, --tools, --add-dir, …) take every following word that does not start with -. The prompt is the argument after this list, so a list that ends- --disallowedTools / - Bash turns the prompt into more values, and the session starts without it. Write --disallowedTools=Bash as one entry instead. In work-item mode the appended token closes such a flag for you, but in cli mode nothing does.
The models
The models list declares the models a work item may be put on, in each provider's own naming convention. Unset — the default — offers no model choice, exactly as before this key existed.
An entry is either a bare name (the common case) or a mapping that narrows it:
models:
- opus-5 # candidate for every declared harness
- fable-5.1
- name: gpt-5.6-sol
harnesses: [cursor] # narrowed: offered and probed on cursor only
about: for the OpenAI-shaped workA name matches ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$. That grammar is the guard, not decoration: the name is passed to the harness's own model flag verbatim, and this is what makes copying a provider's spelling safe — a name can never be a flag, a path or a shell fragment. An entry that declares no usable name contributes nothing, so a fault can only ever shrink the set.
A name is not tied to a harness. Which harness can run which name is measured:
claude cursor
opus-5 ok refused
fable-5.1 ok refused
gpt-5.6-sol refused okThere is deliberately no args escape hatch on a model. A harness with no model flag cannot be handed a model at all, so it is offered no model section rather than given a hand-written flag that nothing validates.
models[].name
- Type:
string - Default: none — required in the mapping form; a bare string entry is the name
The model's name as its provider spells it. the-loop neither normalises it nor parses it for a vendor.
models[].harnesses
- Type:
string[] - Default: unset — the model is a candidate for every declared harness
Narrow this model to these declared harnesses: it is offered only on them, and probed only against them — which is also how you keep the probe matrix small. A harness named here that you did not declare in harnesses contributes nothing.
This narrows; it never confirms. A harness that refuses the model still refuses it, so a declaration here can never assert support that does not exist.
models[].about
- Type:
string - Default:
""
One line rendered beside the model's row on the phase-selection checklist — what a human picking between two models wants to know.
The effort levels
effort
- Type:
string[]— each one oflow,medium,high,xhigh,max,ultra - Default:
[]— unset offers no effort choice
Which of the-loop's effort levels this instance offers. There are no flags in this section, by design: an operator writing a per-harness effort flag is how two configs come to disagree about what high means. The vocabulary is one enum; each adapter translates it.
The enum is the union of what the harnesses the-loop knows about can express, which is what makes it a normalisation rather than an invention:
| the-loop | Claude Code | Codex |
|---|---|---|
low | low | Low (its default) |
medium | medium | Medium |
high | high | High |
xhigh | xhigh (its default) | Extra high |
max | max | Max |
ultra | — | Ultra |
Two rows carry the argument for the whole design. xhigh is one concept with two spellings — which is exactly why an operator should not be writing per-harness flags. ultra is a level only one harness has, so it is simply not offered for the other: a harness that cannot express a level returns no arguments for it, and the row never appears on that work item's checklist.
A level a given harness cannot express is simply not offered for a work item on that harness, and the-loop models check says which levels each harness supports.
No adapter ships an effort mapping yet
Neither claude nor cursor-agent exposes a CLI flag for thinking effort today, so declaring effort currently offers nothing. the-loop does not invent a flag: a mapping is added when a harness has one, read from that CLI's own --help, and then validated by the same probe that validates a model. Declaring the section now is harmless and starts working the day an adapter gains a mapping.
Checking what your declarations actually buy you
$ the-loop models check
harness kind name verdict checked
claude model opus-5 ok 2m ago
claude model fable-5.1 ok 2m ago
claude model gpt-5.6-sol refused 2m ago
cursor model gpt-5.6-sol ok 2m ago
claude effort high refused (no mapping on this adapter)Each row is one non-interactive run of the harness with a fixed the-loop prompt — never any text from a work item — and the verdicts are cached for 24 hours, keyed by the argv probed, so a changed flag or a changed mapping asks again. unknown (the harness could not be run at all) stays offerable: a machine with no binary installed must not silently lose a capability you declared.
What a work item does with them
The phase-selection checklist grows two independent sections — one per offerable model, one per offerable effort level — answered by the same authorized the-loop execute that already freezes the phases, the outer-loop surface and sessionPerPr. The choice is recorded on the work item, applied at every spawn and respawn, and shown by the-loop sessions list.
A work item that picks nothing runs on harnesses[].args alone, exactly as every work item did before this feature existed.