Adapting the profile
Four private files adapt the harness to one user and one machine. None of them is in the repository. They are in ~/.config/research-harness/, and examples/ holds an example of each, with a comment on every key that says what its value is.
| file | what it holds | required | its path | read by |
|---|---|---|---|---|
profile.toml, the profile | every value that names a person, an institution or a machine | yes | harness --profile F, otherwise $RESEARCH_HARNESS_PROFILE, otherwise ~/.config/research-harness/profile.toml | the subcommands that The keys of the profile names for each key |
models.toml, the model tables | the model of each tier for each frontend | for install | harness --models F, otherwise $RESEARCH_HARNESS_MODELS, otherwise beside the profile | install only |
providers.toml | the model servers and accounts of OpenCode and oh-my-pi | no | beside the profile | install only |
network.toml, the network overlay | your own hosts for the network lists of Claude Code | no | beside the profile | install, render of the settings template, settings |
harness setup creates the profile and copies the model tables. To do it by hand, copy each example that you need and write your values into the copy, as the step The profile and the model tables of setup-reference.md shows. A template of the repository names a profile key as {key}, and a subcommand renders the template with the profile before it uses it. A template that names a key that the profile does not have stops the subcommand, and the message names the key.
Read a value
harness get prints the value of one key, one line for each item of a list. harness render prints a template rendered with the profile. Run these commands in the checkout:
harness get repository_roots
harness render launchagents/kaimon.plistBoth subcommands read every key that they name.
The home directory and ~
The key home is optional. Without it, home is the home directory of the user who runs harness. A given home must be an absolute path without ~; otherwise the subcommand stops, and the message names the key. A trailing slash is removed.
A string value that is ~, or that starts with ~/, means home: ~ becomes home, and ~/Research becomes home followed by /Research. The same holds for each such item of a list. Any other ~ stays as written, a~b and ~name/x too. So examples/profile.toml works as copied, on every machine.
One profile for every machine
The template host is the machine whose Claude Code memory is the template for the other machines. The key autocommit_branch names the branch of the origin of ~/.claude that claude-autocommit pushes to: main on the template host, and a branch of its own on each other machine. The optional key template_host names the template host by its short host name: the host name up to its first ., as hostname -s prints it. Without autocommit_branch in the profile, its value is:
mainwhen the profile has notemplate_host;mainon the machine whose short host name istemplate_host;memory/<short host name>on every other machine.
The comparison is exact, so Alpha and alpha are two machines, and an empty host name gives main. A given autocommit_branch is the branch on every machine. A template_host that is empty, holds a . or is not a string stops the subcommand, and the message names the key. Every subcommand reads the same value, and harness get autocommit_branch prints it. So with template_host and no autocommit_branch, one profile works unedited on every machine of one user: write it once, keep it in a private place, and copy it to each machine.
The keys of the profile
The table names each key of examples/profile.toml and the subcommands that read it, in addition to harness get and harness render. The keys memory_project and tree_agents, each a string, and org, repository_roots and leak, each a list of one string or more, are required. Every other key is optional, and the table gives its value without the key.
Every subcommand that reads the profile checks it first. An unknown key, a table other than [setup] among them, a missing required key, a value of the wrong type and an empty required list stop the subcommand with exit status 2. The message names the file, the key and, except for an unknown key, the type that the key takes. An org entry that is no GitHub account name (letters, digits and single hyphens, 1 to 39 characters, not starting or ending with a hyphen) stops it too, and the message names the entry. The retired keys extra_domain, opencode_providers and omp_providers stop it too, and the message names the file that replaces each. A develop at the top level stops it as well, and the message says to move the line into [setup]. doctor reports each of these as a FAIL of its profile check, which quotes the message.
| key | what it holds | read by |
|---|---|---|
home | the home directory, an absolute path. Optional: without it, the home directory of the user who runs harness | install and settings |
harness | the path of the checkout of this repository. Optional: without it, the checkout that runs harness | install, in the OpenCode configuration |
memory_project | a label, not a path: the settings set autoMemoryDirectory to ~/.claude/projects/<memory_project>/memory, so Claude Code keeps the tree's memory there, whatever the tree's path is. The same value on every machine that shares one memory pool, such as Claude Code's name of the tree's project on the first machine (its path with each / replaced by -); it need not match the path on another machine | install and settings, in the settings template |
tree_agents | the directory of the tree instructions | install, which installs them into ~/.claude/ |
launchd_prefix | the reverse-domain prefix of the labels of the launchd jobs in launchagents/, such as org.example: the label of the Kaimon server is <launchd_prefix>.kaimon. macOS only; nothing on Linux reads it. Optional: without it, a template that names it stops the subcommand | render of the launchagents/ templates; doctor; the update job scripts/julia-update.jl on macOS |
template_host | the short host name of the template host. Optional: without it, autocommit_branch is main on every machine | every subcommand that reads the profile, for the value of autocommit_branch |
autocommit_branch | the branch of the origin of ~/.claude that claude-autocommit pushes to: main on the template host, a branch of its own on each other machine. Optional: without it, main on the machine of template_host or without template_host, otherwise memory/<short host name>, as One profile for every machine says | render of launchagents/claude-autocommit.plist and systemd/claude-autocommit.service |
claude_remote | the URL of the git repository of ~/.claude, starting with https:// or git@; any other value stops the subcommand. With the component jobs, harness setup clones it into a ~/.claude that is missing or empty, and the autocommit job pushes to it. Optional: without it, harness setup sets up no autocommit job, and in a terminal it asks for the URL | setup |
org | the GitHub accounts and organisations where a session can open and comment on a pull request or an issue with no prompt | install and settings, in the settings template; leaks, which also searches for each of them |
repository_roots | the directories that hold the repositories of the research tree | leaks, which searches for the name of each directory below them |
docs_exceptions | the repositories that keep a documentation workflow of their own. Optional: without it, none | workflows; ci-protection, through githooks/verify-workflows.jl |
docs_additions | the repositories that add steps to the canonical documentation workflow. Optional: without it, none | workflows; ci-protection, through githooks/verify-workflows.jl |
protected_dir | the directories below home that no session can edit, each as a path relative to home, such as Nextcloud. Optional: without it, none | install, in the settings template and the OpenCode configuration; settings |
leak | the strings that must never appear in the repository, such as your user name and the hosts of your institution. The item ~ stands for the path of your home directory, as each ~ of the profile does | leaks |
[setup] | the choices of this machine, a table with the keys of The setup table. Optional: without it, every frontend and every component, and develop false | install, doctor, setup and get develop |
The questions of harness setup
Without a profile, harness setup asks for each required key; in a terminal, it then asks for the frontends, the components and develop, also with a profile, and with the component jobs for claude_remote where the profile has none. Before each question it prints what the answer is, and whether the answer is the same on every machine that shares the research tree:
| question | what it is | across machines |
|---|---|---|
memory_project | a label, not a path: Claude Code keeps the tree's memory in ~/.claude/projects/<label>/memory | the same on every machine that shares one memory pool; usually the tree's path on the first machine, with each / replaced by - |
tree_agents | the directory of the tree instructions, which harness install copies into ~/.claude/ | the same when written with ~; an absolute path differs with the home directory |
org | the GitHub accounts and organisations, your own included, where a session may open and comment on pull requests and issues without a prompt | the same |
repository_roots | the directories that hold the tree's repositories; harness leaks searches the harness for the name of each directory below them | the same when written with ~ |
leak | the strings that must never appear in the harness repository, such as user names and host names; the item ~ stands for the path of the home directory | mostly the same; add the user and host names of each machine |
| frontends | the agent programs whose configuration harness install writes: claude is Claude Code, opencode OpenCode, omp oh-my-pi and antigravity the Antigravity desktop app | per machine |
| components | what harness setup installs and starts beside the frontends: julia (juliaup, Julia, fatou, Kaimon and the Julia environment of the scripts), rtk, jq, jobs (Kaimon, the Julia update and the autocommit of ~/.claude as background jobs) and dotfiles (chezmoi) | per machine |
develop | only for a machine that changes the harness's own code: it adds the dev group of uv.lock, the tools that check the Python code, such as ruff and basedpyright | per machine |
claude_remote | asked with the component jobs when the profile has no claude_remote: the URL of the git repository of ~/.claude, which harness setup clones there and the autocommit job pushes to; an empty answer means no autocommit job | the same |
An empty answer keeps the example's value. A list takes either items separated by commas, such as a, b, or a TOML list, such as ["a", "b"], and the answer replaces the example's list. An item that holds a bracket or a quote, a TOML list that does not parse, holds a value that is not a string or holds an empty string, an answer with no item, and an org item that is no GitHub account name are refused with one line, and the question comes again; so is a claude_remote that starts with neither https:// nor git@. harness setup writes a claude_remote answer as the first line of the profile. harness setup --yes asks nothing and prints none of this.
The setup table
The table [setup] holds what this machine gets. Put it at the end of the profile, as examples/profile.toml does: in TOML, each key after a table header belongs to the table. Without the table, every frontend and every component is chosen and develop is false. With the table, frontends and components are required.
| key | what it holds | read by |
|---|---|---|
frontends | the frontends whose layers harness install plans and installs, each once: claude, opencode, omp and antigravity, in any order. An empty list installs no layer. A layer of a frontend that the list leaves out stays on disk: install removes nothing, and doctor reports it as a WARN that names [setup] and the frontend | install; doctor, whose OpenCode directory check is SKIP without opencode; setup, which installs each frontend |
components | the components of this machine, each once: julia, rtk, jq, jobs and dotfiles. With julia, install installs the Julia environment of the scripts and doctor checks its packages; without it, neither does, and the packages are SKIP. setup installs the tools of each component | install; doctor; setup |
develop | true on a machine that develops the harness: the Python environment of the harness then also holds the dev group of uv.lock, the tools that check the Python code. Optional: without it, false | install, which syncs the environment; doctor, which checks it; get develop, which prints it |
A key of [setup] that the table does not show, a missing frontends or components, a value of the wrong type, a name that the table does not list and a name given twice stop every subcommand that reads the profile with exit status 2, and the message names the file, [setup] and the key or the name.
install reads the settings template too, because it writes the permission layer of oh-my-pi from it. So each key of the settings template is a key that install reads.
leaks also renders every template of the repository, but with examples/profile.toml, the home /home/example and the host name example-other, not with your profile. From your profile it reads only leak, org and repository_roots.
The tables of the model tables
An agent or a skill names a tier in its model:: large, medium or small. Only harness install reads the model tables. It maps each tier to the model of each frontend.
| table | what it holds | read by |
|---|---|---|
[claude] | the Claude Code model of each tier, written as the model: of the installed agent or skill; all three tiers are required | install |
[models] | the OpenCode and oh-my-pi model of each tier, as provider/model; the medium model is also oh-my-pi's default model, in harness.yml, and oh-my-pi needs all three tiers | install |
[model_overrides] | an agent whose model is not the model of its tier | install |
[model_variants] | the effort of every agent on one model: OpenCode's variant:, oh-my-pi's thinking-level | install |
[variants] | the effort of one agent or council seat, over the effort of its model | install |
[reasoning_effort] | the effort of one agent or council seat, for a model that has no variant for it: OpenCode's reasoningEffort:, oh-my-pi's thinking-level | install |
[councils] | the copies of an agent on other models; one seat may add verify = true, and then it also judges each verify round | install |
[context_limits] | the input tokens that a model of a built-in provider may hold: a provider entry in opencode.jsonc, contextWindow in oh-my-pi's models.yml. A provider of providers.toml stops install with exit status 2: its model holds the limit as context | install |
[opencode], [omp] | the same sub-tables, such as [omp.models]; each key replaces the shared key for that frontend alone | install |
OpenCode and oh-my-pi share every table but [claude] and [antigravity], Antigravity's table of the tiers, so that their agents run on the same models.
A table that names a tier by an old name, opus, sonnet or haiku, stops harness install with exit status 2, an error, and the message names the rename. install then checks the whole file before it plans any frontend: a table that no frontend reads, a key that a table does not name, a [claude] without one of the three tiers and a value of the wrong type stop it with exit status 2, and the message names the file, the table and the key. Each table is optional, and a missing one is empty; a frontend that needs it stops install then, as [claude] does, and [antigravity] once ~/.gemini/ exists. architecture.md shows how each frontend reads a tier.
The providers
providers.toml beside the profile lists, once each, the model servers and accounts that are not built into OpenCode and oh-my-pi. install writes each provider into the provider object of OpenCode's opencode.jsonc and into oh-my-pi's models.yml. The file is optional. examples/providers.toml shows it. A table is one provider, named as models.toml names it in provider/model.
| key | what it holds | OpenCode | oh-my-pi |
|---|---|---|---|
api | "anthropic" or "openai-compatible" | npm: @ai-sdk/anthropic, @ai-sdk/openai-compatible | api: anthropic-messages, openai-completions |
url | the base URL, with its /v1 | options.baseURL | baseUrl, without the /v1 for "anthropic" |
name | the name that the frontend shows; optional | name | — |
key | {env:VAR} or {file:PATH}; optional. A literal key stops install with exit status 2 | options.apiKey, as written | apiKey: VAR, or !cat PATH |
frontends | the frontends that get the provider; optional, both by default | ||
[<name>.models."<id>"] | a model: name, reasoning, and context with output, each optional | models.<id>: name, reasoning, limit | an item of models: id, name, reasoning (false when the model has none, so that oh-my-pi sends no effort, as OpenCode does), contextWindow, maxTokens |
effort of a model | the reasoning effort that the model always gets, such as "none"; optional | options.reasoningEffort | reasoning = true, and in compat a reasoningEffortMap that maps each thinking level to it and reasoningDisableMode = "none-effort"; an agent that turns thinking off sends "none" |
opencode, omp | a frontend's own keys, of a provider or of a model, merged over the rest | merged | merged |
A key that the table does not name stops install with exit status 2. The profile key opencode_providers or omp_providers stops every subcommand that reads the profile: move its providers into providers.toml.
The network overlay
The settings template names no host. settings/network.toml holds the hosts of the harness, and the overlay network.toml beside the profile adds your own, such as the library resolver of your institution. The overlay is optional. examples/network.toml shows it. Both files have the same keys, and each key is optional in the overlay.
| key | what it holds | what the install writes |
|---|---|---|
allow | the hosts that the WebFetch tool and a sandboxed command can reach; write a host, never *.X | WebFetch(domain:X) in permissions.allow; X in sandbox.network.allowedDomains, and *.X too when X is its own registrable domain |
infrastructure | the hosts that only a sandboxed command can reach, such as a package registry; *.X is allowed | X in allowedDomains, as written |
deny | the hosts that neither can reach; *, *.X and X:port are allowed | X in sandbox.network.deniedDomains; WebFetch(domain:X) in permissions.deny, except for an entry with a port |
[deny_reasons] | a reason for a deny entry, keyed by the exact entry | sandbox.network.deniedDomainReasons, which Claude Code shows the model when the sandbox refuses the host |
install, render of the settings template and settings read both files. A deny entry of either file removes each allow and infrastructure host that it matches from the generated allow lists. A deny entry with a port leaves the host on the allow lists. The sandbox refuses a host that no list names, with no prompt, because the template sets strictAllowlist. A WebFetch of such a host is on no list either, so it asks in the default permission mode.
These cases stop the subcommand with exit status 2:
- an entry that is a URL;
- an entry with a port outside
deny; - a host in both
allowanddenyof one file; - a key that the table does not name.
The profile key extra_domain stops every subcommand that reads the profile: move its hosts into the overlay's allow.
OpenCode and oh-my-pi have no network allowlist. For Antigravity, harness install writes each allow host as read_url(https://<host>/*).