Skip to content

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.

filewhat it holdsrequiredits pathread by
profile.toml, the profileevery value that names a person, an institution or a machineyesharness --profile F, otherwise $RESEARCH_HARNESS_PROFILE, otherwise ~/.config/research-harness/profile.tomlthe subcommands that The keys of the profile names for each key
models.toml, the model tablesthe model of each tier for each frontendfor installharness --models F, otherwise $RESEARCH_HARNESS_MODELS, otherwise beside the profileinstall only
providers.tomlthe model servers and accounts of OpenCode and oh-my-pinobeside the profileinstall only
network.toml, the network overlayyour own hosts for the network lists of Claude Codenobeside the profileinstall, 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:

bash
harness get repository_roots
harness render launchagents/kaimon.plist

Both 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:

  • main when the profile has no template_host;
  • main on the machine whose short host name is template_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.

keywhat it holdsread by
homethe home directory, an absolute path. Optional: without it, the home directory of the user who runs harnessinstall and settings
harnessthe path of the checkout of this repository. Optional: without it, the checkout that runs harnessinstall, in the OpenCode configuration
memory_projecta 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 machineinstall and settings, in the settings template
tree_agentsthe directory of the tree instructionsinstall, which installs them into ~/.claude/
launchd_prefixthe 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 subcommandrender of the launchagents/ templates; doctor; the update job scripts/julia-update.jl on macOS
template_hostthe short host name of the template host. Optional: without it, autocommit_branch is main on every machineevery subcommand that reads the profile, for the value of autocommit_branch
autocommit_branchthe 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 saysrender of launchagents/claude-autocommit.plist and systemd/claude-autocommit.service
claude_remotethe 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 URLsetup
orgthe GitHub accounts and organisations where a session can open and comment on a pull request or an issue with no promptinstall and settings, in the settings template; leaks, which also searches for each of them
repository_rootsthe directories that hold the repositories of the research treeleaks, which searches for the name of each directory below them
docs_exceptionsthe repositories that keep a documentation workflow of their own. Optional: without it, noneworkflows; ci-protection, through githooks/verify-workflows.jl
docs_additionsthe repositories that add steps to the canonical documentation workflow. Optional: without it, noneworkflows; ci-protection, through githooks/verify-workflows.jl
protected_dirthe directories below home that no session can edit, each as a path relative to home, such as Nextcloud. Optional: without it, noneinstall, in the settings template and the OpenCode configuration; settings
leakthe 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 doesleaks
[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 falseinstall, 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:

questionwhat it isacross machines
memory_projecta label, not a path: Claude Code keeps the tree's memory in ~/.claude/projects/<label>/memorythe same on every machine that shares one memory pool; usually the tree's path on the first machine, with each / replaced by -
tree_agentsthe directory of the tree instructions, which harness install copies into ~/.claude/the same when written with ~; an absolute path differs with the home directory
orgthe GitHub accounts and organisations, your own included, where a session may open and comment on pull requests and issues without a promptthe same
repository_rootsthe directories that hold the tree's repositories; harness leaks searches the harness for the name of each directory below themthe same when written with ~
leakthe 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 directorymostly the same; add the user and host names of each machine
frontendsthe agent programs whose configuration harness install writes: claude is Claude Code, opencode OpenCode, omp oh-my-pi and antigravity the Antigravity desktop appper machine
componentswhat 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
developonly 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 basedpyrightper machine
claude_remoteasked 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 jobthe 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.

keywhat it holdsread by
frontendsthe 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 frontendinstall; doctor, whose OpenCode directory check is SKIP without opencode; setup, which installs each frontend
componentsthe 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 componentinstall; doctor; setup
developtrue 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, falseinstall, 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.

tablewhat it holdsread by
[claude]the Claude Code model of each tier, written as the model: of the installed agent or skill; all three tiers are requiredinstall
[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 tiersinstall
[model_overrides]an agent whose model is not the model of its tierinstall
[model_variants]the effort of every agent on one model: OpenCode's variant:, oh-my-pi's thinking-levelinstall
[variants]the effort of one agent or council seat, over the effort of its modelinstall
[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-levelinstall
[councils]the copies of an agent on other models; one seat may add verify = true, and then it also judges each verify roundinstall
[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 contextinstall
[opencode], [omp]the same sub-tables, such as [omp.models]; each key replaces the shared key for that frontend aloneinstall

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.

keywhat it holdsOpenCodeoh-my-pi
api"anthropic" or "openai-compatible"npm: @ai-sdk/anthropic, @ai-sdk/openai-compatibleapi: anthropic-messages, openai-completions
urlthe base URL, with its /v1options.baseURLbaseUrl, without the /v1 for "anthropic"
namethe name that the frontend shows; optionalname—
key{env:VAR} or {file:PATH}; optional. A literal key stops install with exit status 2options.apiKey, as writtenapiKey: VAR, or !cat PATH
frontendsthe frontends that get the provider; optional, both by default
[<name>.models."<id>"]a model: name, reasoning, and context with output, each optionalmodels.<id>: name, reasoning, limitan 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 modelthe reasoning effort that the model always gets, such as "none"; optionaloptions.reasoningEffortreasoning = 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, ompa frontend's own keys, of a provider or of a model, merged over the restmergedmerged

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.

keywhat it holdswhat the install writes
allowthe hosts that the WebFetch tool and a sandboxed command can reach; write a host, never *.XWebFetch(domain:X) in permissions.allow; X in sandbox.network.allowedDomains, and *.X too when X is its own registrable domain
infrastructurethe hosts that only a sandboxed command can reach, such as a package registry; *.X is allowedX in allowedDomains, as written
denythe hosts that neither can reach; *, *.X and X:port are allowedX 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 entrysandbox.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 allow and deny of 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>/*).