Skip to content

The harness command ​

Synopsis ​

text
harness [--profile F] [--models F] <subcommand> [options] [arguments]
harness --help
harness <subcommand> --help

Description ​

bin/harness is the entry point of the harness. It starts on Python 3.9 or later and needs nothing beyond the standard library. Put bin/ on the PATH, as the step The clone of setup-macos.md and setup-linux.md shows. A Python below 3.9 prints the version that it found, 3.9 and 3.14, and exits 2. Every subcommand runs on the interpreter of the Python environment of the harness, Python 3.14 or later, which bin/harness makes with uv the first time that it runs outside a session. Where the Python that starts it is below 3.14, uv fetches Python 3.14 for the environment; a session with no environment cannot, and exits 2.

Each subcommand of harness, such as install or doctor, keeps one contract:

  • A subcommand that changes something is dry by default. It prints its plan, and exits 1 when something would change, 0 when nothing would. --apply makes the change, and exits 0.
  • A read-only subcommand takes no mode flag. It exits 0, or 1 where its section says so.
  • Exit 2 is a usage error or a failure, with one line on standard error: harness <subcommand>: <reason>.

So a dry run in a script tells you whether there is work to do: exit 1 means "run it again with --apply". setup is the one exception: it has no --apply, because its y/N in a terminal, or --yes in a script, is the confirmation, and --dry-run prints its plan. The two checks differ: harness test exits 1 when a test case fails, and harness doctor exits 1 when a check fails.

development.md describes the cases of harness test, and architecture.md the Claude Code layer that harness install writes.

Global options ​

These options come before the subcommand.

optionmeaning
--profile Fthe private profile. If it is not given, the profile is $RESEARCH_HARNESS_PROFILE, or ~/.config/research-harness/profile.toml if that is not set. examples/profile.toml shows every key
--models Fthe model tables. They map each model tier to its model in each frontend, which is a tier of Antigravity's own in Antigravity, and an agent to its own model. If the option is not given, the model tables are $RESEARCH_HARNESS_MODELS, or models.toml beside the profile if that is not set. examples/models.toml shows the layout
-h, --helpprint the list of subcommands. harness <subcommand> --help prints the options of one subcommand

Subcommands ​

subcommanddoeschanges something
setupinstalls the tools and frontends that the profile's [setup] chooses, then runs install --apply, trust --apply, the jobs and doctoryes, after y/N or with --yes
installinstalls the Julia environment of the scripts, syncs the Python environment of the harness, and installs the Claude Code layer with the sections permissions, hooks, sandbox and modelSettings of settings.json, the OpenCode configuration, the oh-my-pi permission layer, instructions, rules, agents, models and MCP entry, and the Antigravity guard hook, permission rules, instructions, rules, skills, agents and MCP entryyes
updatebrings the harness checkout up to origin/main, then runs install --apply. It does not move a checkout that is pinned to a commityes
doctorchecks the setup of this machine: the tools, the profile, the install and the jobsno
trustsets Claude Code workspace trust for every repository of the treeyes
dotfiles-kitcopies the dotfiles kit of the harness into a chezmoi sourceyes
cloneclones each repository of the repository table that has no directory yetyes
repos statusreports each repository: its branch, its commits ahead and behind, its changed files and its open pull requestsno
repos pullfast-forwards each repository that is behind its upstreamyes
repos pushpushes each repository that is ahead of its upstream, one at a timeyes
repos syncrepos pull, then repos pushyes
githooksinstalls the shared git hooks into every package and experimentyes
wiki-lintlints the Markdown of the prose repositories: links, paths, line citations, frontmatterno
wiki-lint-hookinstalls the wiki-lint hook into the prose repositoriesyes
workflowsinstalls the canonical GitHub workflows into every package and experimentyes
ci-protectionprotects every default branch on GitHub with a rulesetyes
formatruns JuliaFormatter over every tracked .jl file of the packages and experimentsyes
renderprints a template rendered with the profileno
getprints a profile valueno
leakssearches the repository for the private strings of the profileno
settingsmeasures and checks the Claude Code settings: surface, compare, twins, selftestno
permissionsgenerates the OpenCode permission block and path-guard list from the settings templateyes
testruns the harness's own test casesno
mutateruns named mutants of the Python code against the parts of test that should kill themno
changelogjoins the changelog fragments of changelog.d/ into CHANGELOG.md, or checks that a branch adds oneyes
skill-triggerstests whether each skill loads on its queriesruns paid sessions with --apply

The sections below give each subcommand's synopsis, what it does, its options, its exit status, the files that it reads and writes, where to run it, and examples. Where to run it says whether an agent session can run the subcommand. A session runs in the Claude Code sandbox, which refuses some writes and some network calls; a subcommand that needs them runs in your own terminal.

Set up and keep a machine ​

setup ​

text
harness setup [--yes] [--dry-run]

Sets up a machine in one run: the tools and the frontends that the profile's [setup] chooses, then the harness's own steps. The machine needs Python 3.9 or later (uv fetches Python 3.14 below it), uv, git and the clone before it, and on macOS Homebrew. It runs on macOS and on Ubuntu, on the architectures arm64 and x86_64 of macOS and x86_64 and aarch64 of Linux.

In a terminal, setup asks its questions. Without a profile, it copies examples/profile.toml and asks only for the keys that have no default, memory_project, tree_agents, org, repository_roots and leak; an empty answer keeps the example's value. It then asks for the frontends and the components, each from a numbered list, and whether this machine develops the harness (develop), y/N. It shows the plan and runs it after y/N. The plan's first step writes the answers into [setup] of the profile. A missing models.toml beside the profile is copied from examples/models.toml.

The plan holds these steps, in this order:

  1. the directories that install needs: the tree instructions, each repository_roots entry, and with opencode the OpenCode configuration directory;
  2. the packages. On macOS, one brew install line of the missing tools and of the tools below their minimum that Homebrew does not list as installed, and one brew upgrade line of the tools below their minimum that it lists. On Ubuntu, one sudo apt-get install -y line of each package whose apt candidate meets the tool's minimum;
  3. on Ubuntu, each tool that apt does not meet, from its pinned release of setup/tools.toml into ~/.local/bin, after a check of the sha256 of the archive; the release of Node.js is a whole tree, unpacked into ~/.local/share/research-harness/node/<version>/, and node, npm and npx in ~/.local/bin link into it;
  4. the vendor installers, in this order: juliaup, Julia through juliaup when juliaup is there (juliaup update when Julia is below its minimum), fatou on Ubuntu, Kaimon as a Julia app, Claude Code, and on Ubuntu OpenCode (with --no-modify-path) and oh-my-pi;
  5. harness install --apply;
  6. git config core.hooksPath .githooks in the clone;
  7. harness trust --apply;
  8. with the component jobs, the jobs as the dotfiles kit's services step does them: the copies in ~/.local/bin, then the launch agents or the systemd --user units, rendered with harness render and loaded; on macOS, the Seatbelt profiles of the Kaimon and julia-update jobs, rendered with harness render sandbox/<job>.sb into ~/.config/research-harness/sandbox/ before each one's launch agent, and before the Kaimon job's profile the ps shim sandbox/shims/ps, mode 755, into that directory's shims/;
  9. with the component julia, the Kaimon token ~/.config/kaimon/auth-token, mode 600, from the first key of ~/.config/kaimon/config.json when that file exists and the token does not;
  10. harness doctor, after the lines under By hand, so that its report ends the run.

A tool that is installed at or above its minimum of dependencies.md is skipped; a tool with no minimum, only a version that it is tested with, is met by any version. After a Homebrew line or juliaup update for a tool below its minimum, setup reads the tool's version again. When it is still below, the step fails: for Julia, whose juliaup update keeps the default channel, with the line juliaup add release && juliaup default release; for another tool, with the path of the copy that comes first on the PATH. setup/tools.toml holds the minimums and the pinned releases. The OpenCode installer runs only when opencode is not on the PATH, and the juliaup installer only when juliaup is not. setup runs the OpenCode installer with --no-modify-path and prints the PATH line of ~/.opencode/bin instead; the juliaup installer adds its own PATH block to the shell startup files. Each step that setup cannot do, such as the Antigravity app and loginctl enable-linger for the jobs on Linux, is printed under By hand.

optionmeaning
--yesread [setup] of the profile, or the defaults of a profile without it, and ask nothing. It never writes the profile
--dry-runprint every command of the plan and change nothing, not even the profile

Exit status: 0 after the plan ran and harness doctor passed, after the dry run, and when the answer to the plan is no; 1 after the plan ran and harness doctor reported a FAIL; 2 when a step fails, which prints its output first, harness doctor with a status other than 0 or 1 included; when stdin is no terminal and --yes is not given; when --yes finds no profile; when [setup] cannot be written into the profile in place, naming the table to write; when an answer is not valid UTF-8; on Ubuntu when apt-cache fails, and without sudo, or when sudo -n true is refused, after the apt line is printed for an administrator; and on a system other than macOS and Ubuntu, or an unknown architecture. A second run plans again and starts from the first step; each step skips what is done.

Reads: the profile, setup/tools.toml, the versions of the installed tools, and on Ubuntu apt-cache policy. Writes: the profile (in a terminal), models.toml when it is missing, the packages and programs that the steps install, and what install, trust and the jobs write.

Where to run it: in your own terminal. A session runs only --dry-run.

bash
harness setup --dry-run --yes   # the plan for [setup], with no question
harness setup                   # ask, show the plan, run it after y/N
harness setup --yes             # the dotfiles kit's harness step

install ​

text
harness install [--apply] [--force]

Installs the Julia environment of the scripts, syncs the Python environment of the harness, and installs the configuration of each frontend that the profile's [setup] chooses: Claude Code, OpenCode, oh-my-pi and Antigravity. Without [setup], it installs all four and the Julia environment; with it, the Julia environment only with the julia component, and the Python environment always. A layer of a frontend that [setup] does not choose is neither planned nor removed. The installed configuration is not writable from a session, so the sources live in the harness repository, and install copies them into place. It plans every chosen frontend first and writes nothing while it plans. So a refusal stops the install before the first write. After --apply, it prints the restart line of each frontend whose files changed.

optionmeaning
--applyinstall; without it, print the plan
--forcereplace a file that the plan refuses to replace; the refusal names the file

Exit status: 0 when nothing would change or --apply installed; 1 when the dry run has a change; 2 when a plan refuses, or the profile or the model tables do not load.

Reads: the profile, the model tables, providers.toml and network.toml beside the profile, the tree instructions (tree_agents of the profile), and the sources of the harness checkout. Writes: for each chosen frontend, below ~/.claude/, the OpenCode configuration directory, the oh-my-pi directory, and ~/.gemini/ when Antigravity has made it; the Julia environment with the julia component (see The Julia environment); and the Python environment (~/.local/share/research-harness/python/, with the dev group when [setup] of the profile sets develop = true). With claude chosen, it writes the stamp ~/.claude/.harness-install.json, which the drift warning at session start reads. For each chosen frontend it also writes the srt settings, ~/.config/research-harness/sandbox/<frontend>.json; with any frontend chosen, the launcher ~/.local/bin/agentbox; and with claude chosen, Claude Code's override ~/.config/research-harness/sandbox/claude-inner-off.json (The srt settings).

Where to run it: in your own terminal. A session cannot write the installed files.

install warns when its checkout is behind its origin/main, as the last fetch saw it. It fetches nothing, and it installs the checkout as it is. To fetch, merge and install in one step, use update.

bash
harness install            # the plan
harness install --apply    # install

update ​

text
harness update [--apply]

Brings the harness checkout up to origin/main, and installs it. It works on the checkout that runs it, and only when its branch is main with the upstream origin/main: a development checkout. The dry run fetches, prints the new commits and the files that they change, and exits 1 when there is a new commit. --apply also shows the whole diff, through git's pager, and asks y/N. On y it merges --ff-only and runs harness install --apply from the merged checkout. Any other answer merges and installs nothing.

A checkout at a detached HEAD is pinned to a commit, for example by the dotfiles of a machine. Only a reviewed change of the pin moves it, so update stops there and moves nothing.

optionmeaning
--applyshow the diff, ask, merge and install

Exit status: 0 when the checkout is up to date, or after --apply; 1 when the dry run finds a new commit; 2 when the checkout is pinned, is on another branch, tracks another upstream, has commits that origin/main lacks, cannot fetch, has changes to tracked files (--apply), or has no terminal for the question (--apply), and when install --apply fails after the merge.

Where to run it: in your own terminal. --apply asks a question, and the install writes files that a session cannot write.

bash
harness update            # fetch and list the new commits
harness update --apply    # read the diff, answer y, merge and install

doctor ​

text
harness doctor [--full]

Checks this machine's setup and changes nothing. It prints one line per check: PASS, WARN, FAIL or SKIP, the name of the check and a detail. A FAIL line also names the section of the setup pages that fixes it. The checks are, in this order: the machine; the tools of dependencies.md, each with its minimum version; the profile; the dry run of install and its warnings; and the jobs (Kaimon, the update job, the autocommit job). Where the profile's [setup] does not choose opencode, the OpenCode directory check is a SKIP, and without the julia component so are the Julia packages. A layer of a frontend that [setup] does not choose and that is still on disk is a WARN that names [setup] and the frontend, and so is such a frontend whose plan refuses, with the refusal; doctor removes nothing.

optionmeaning
--fullalso run harness test and julia -e 'using Revise', which take minutes

Exit status: 1 when a check fails, else 0. A WARN or a SKIP does not count.

Where to run it: in a plain terminal. In a session, the sandbox refuses some commands, and those checks are SKIP with the reason.

bash
harness doctor
harness doctor --full

trust ​

text
harness trust [--apply]

Marks the research root and every git repository below it as trusted in Claude Code's ~/.claude.json, so that Claude Code shows no trust dialog. The dry run lists each untrusted root.

Exit status: 0 when every root is trusted, or after --apply; 1 when the dry run finds an untrusted root; 2 when ~/.claude.json cannot be read or is not a JSON object.

Where to run it: in your own terminal. A session cannot read ~/.claude.json. Run it again after clone: a new clone is not trusted.

dotfiles-kit ​

text
harness dotfiles-kit [--apply] DEST

Compares the chezmoi source DEST with the dotfiles kit of the harness, examples/dotfiles/. The file examples/dotfiles/kit.toml names each file of the kit and its class. A code file stays identical to the kit, so the dry run lists each code file that is missing from DEST, or differs in its bytes or in its execute bit. A seed file belongs to DEST once it is there, so the dry run lists each seed file that is missing, and --apply never writes a seed file that exists. A file of DEST that kit.toml does not name is not listed and not changed.

DEST is a clone of your dotfiles repository. dotfiles-kit refuses the source that chezmoi applies, ~/.local/share/chezmoi, and each directory below it, under any name of the same directory: a symbolic link, .., a case variant or a firmlink. A change of the applied source goes through a branch and a merge request. Every file that dotfiles-kit lists or writes stays inside DEST: when the directory of a file of kit.toml links out of DEST, the dry run and --apply stop before the first write and name the file. A symbolic link at a code file's own path is replaced by the file; anything else there that is not a regular file, such as a directory or a FIFO, stops --apply.

optionmeaning
--applycopy the listed files; without it, print the list

Exit status: 0 when nothing would change, or after --apply; 1 when the dry run lists a file; 2 when DEST is not a directory or is the applied source, when kit.toml cannot be read or names a file that the harness lacks or a class other than code and seed, when the directory of a file resolves outside DEST, when a code file of DEST cannot be read, and when a file cannot be written.

Where to run it: where you can write DEST. Commit the change on a branch of the clone. The dotfiles command gives the steps of a private copy, and the scripts that the kit installs.

bash
harness dotfiles-kit ~/src/dotfiles            # the list
harness dotfiles-kit --apply ~/src/dotfiles    # copy

The repositories of the research tree ​

These subcommands find the research tree at $RESEARCH_ROOT, or ~/Research if it is not set. The research tree shows them in the order of a working day.

The repository table ​

clone and repos read the repository table: $RESEARCH_HARNESS_REPOSITORIES, or repositories.toml beside the profile if that is not set. It names each repository of the tree and its clone URL:

toml
# A fixed repository: its directory is the key in title case, ~/Research/Knowledge.
knowledge = "https://git.example.org/example/knowledge.git"

# A group: its directory is the table's name in title case, and each key a directory there.
[packages]
Example = "https://github.com/example/Example.jl.git"

[papers]
"A Paper Title" = "https://git.example.org/example/papers/a-paper.git"

The fixed repositories are knowledge, environment, tasks and library. The groups are packages, experiments, papers, books, talks and projects. Another key is an error, and so is a directory name that holds a / or starts with .. examples/repositories.toml shows a whole table.

The table is optional. Without it, repos takes each git repository that it finds directly below the group directories, and each fixed repository that exists; clone stops. Directory names compare in Unicode NFC, so a name that the file system stores decomposed matches the table.

Selections ​

clone and each action of repos take zero or more selections: a group (packages) or a fixed repository (knowledge), in any case. Without a selection, clone, repos status and repos pull take every group and every fixed repository, and repos push and repos sync take packages and experiments. An unknown selection exits 2 and names the valid ones.

clone ​

text
harness clone [SELECTION ...] [--apply]

Clones each repository of the table that has no directory yet. A directory that exists and is a clone of its URL is skipped. Two URLs are the same when they agree without a trailing / and .git. A directory that exists and is not a clone of its URL is in the way: the subcommand names it and stops before the first clone, and nothing is cloned.

optionmeaning
SELECTIONa group or a fixed repository; none means all
--applyclone; without it, list the clones

Exit status: 0 when nothing is to clone, or after --apply; 1 when the dry run lists a clone; 2 when there is no table, the table is wrong, a directory is in the way, or a clone fails.

Where to run it: in your own terminal: a clone needs the network and your credentials. A new clone has no git hooks and no workspace trust. After --apply, the subcommand names the three runs that give them.

bash
harness clone                 # list what a new machine still lacks
harness clone --apply         # clone it all
harness clone papers --apply  # clone the papers only
harness githooks --apply      # then the hooks,
harness wiki-lint-hook --apply
harness trust --apply         # and the workspace trust

repos status ​

text
harness repos status [SELECTION ...] [--no-fetch] [--no-prs]

Fetches every repository that has an origin, eight at a time, then prints one line for each repository: the branch, the commits ahead of and behind its upstream, the number of changed tracked files, and the numbers of the open pull requests. It asks gh for a repository on github.com, and glab for a repository on any other host. A ? means that the host could not be asked; a - means no open pull request. A line also names:

  • MISSING: a repository of the table that is not cloned;
  • not in the table: a git repository in a group directory that the table does not list;
  • no origin and no upstream;
  • origin … is not the table's …: a clone whose origin differs from the table;
  • fetch failed, with the last line of git's error.
optionmeaning
SELECTIONa group or a fixed repository; none means all
--no-fetchread the remote-tracking refs as the last fetch left them
--no-prsask no host for pull requests

Exit status: 0; 2 when the table is wrong or a selection is unknown. The report itself never makes it exit 1.

Where to run it: anywhere. In a session's sandbox, the fetches and the pull-request queries can fail; --no-fetch --no-prs then reports from the local state.

bash
harness repos status
harness repos status packages experiments
harness repos status --no-fetch --no-prs

repos pull ​

text
harness repos pull [SELECTION ...] [--apply]

Fetches, then fast-forwards each branch that is behind its upstream, with git merge --ff-only. It skips a branch with no upstream, a branch that has diverged from its upstream, and a branch whose incoming commits change a file with local changes. A local change to any other file stays as it is. A local change whose content already equals the upstream's is no clash: pull resets the file to HEAD, fast-forwards, and names the file. The dry run fetches too: a fetch changes no working tree.

optionmeaning
SELECTIONa group or a fixed repository; none means all
--applyfast-forward; without it, list the branches that would move

Exit status: 0 when nothing is behind, or after --apply; 1 when the dry run lists a branch; 2 when a merge fails.

Where to run it: in your own terminal, which can reach the network.

bash
harness repos pull
harness repos pull --apply

repos push ​

text
harness repos push [SELECTION ...] [--apply] [--no-verify] [--include-topic-branches]

Fetches, then pushes each branch that is ahead of its upstream, one repository at a time. It checks each push against the remote ref, as git ls-remote reports it, not by the exit status of git push. It skips a branch with no upstream, and a branch that is also behind its upstream: pull or rebase it first. It skips a branch that is not the default branch, because a commit on a topic branch does not reach the required checks. The dry run names up to three .jl files of each push.

The pushes run in sequence, never in parallel. The pre-push hook of a package runs its full test suite, and two suites that load the same package with different settings invalidate each other's precompiled files.

optionmeaning
SELECTIONa group or a fixed repository; none means packages and experiments
--applypush; without it, list the pushes
--no-verify, --skip-hookspush with git push --no-verify: the pre-push hook does not run
--include-topic-branchesalso push a branch that is not the default branch

Use --no-verify only for a push that touches no Julia source and no [compat] entry, such as a workflow, a README or a changelog. CI runs on the push anyway.

Exit status: 0 when nothing is ahead, or after --apply; 1 when the dry run lists a push; 2 when a push fails. A failed push prints the last lines of git's output.

Where to run it: in your own terminal, and expect it to take hours. A pre-push suite takes a few minutes in most packages and 10 to 30 minutes in the large ones, and prints nothing while it runs. A tool call of a session stops after ten minutes. Stopping a push in the middle of its suite leaves a Julia process behind; stop that process too.

bash
harness repos push                           # the plan
harness repos push --apply                   # push, with each pre-push suite
harness repos push experiments --apply --no-verify

repos sync ​

text
harness repos sync [SELECTION ...] [--apply] [--no-verify] [--include-topic-branches]

Runs repos pull, then repos push, on the same selection, with the same options. Without a selection it takes packages and experiments.

Exit status: as repos pull and repos push: 1 when the dry run has something to pull or to push; 2 when a merge or a push fails.

Where to run it: in your own terminal, as repos push.

bash
harness repos sync
harness repos sync --apply

githooks ​

text
harness githooks [--apply]

Copies githooks/pre-commit, githooks/pre-commit.jl, githooks/pre-push, githooks/pre-push.jl and scripts/test-layout.jl into .githooks/ of each git repository below Packages/ and Experiments/, and sets core.hooksPath to .githooks. The copies are tracked files of each repository: read the diff, then commit each file by name. core.hooksPath is local configuration, so a new clone needs this subcommand again.

Exit status: 0 when every copy is current, or after --apply; 1 when the dry run has a change; 2 when core.hooksPath cannot be set. The subcommand then prints the command for each repository.

Where to run it: in your own terminal. A session cannot write .git/config.

wiki-lint ​

text
harness wiki-lint [TREE ...] [--strict] [--quiet]

Checks the structure of the Markdown in the prose repositories of the research tree: the vaults Knowledge and Environment, and the reference trees Tasks and Library, or the trees that TREE names. It finds text that a later change of the tree made false, for example a broken link, a path that does not exist, or a line citation past the end of its file. A vault gets every check. A reference tree gets the checks of paths and line citations; Tasks gets the schema of the task board too. The lint skips each CHANGELOG.md. It reads only the working tree: no model, no index, no network.

A frontmatter is YAML. In the vaults and in Tasks, a block that does not parse is an ERROR on the line that PyYAML names, and so is a key that a block repeats. A key or a value that YAML 1.1 converts, an item of a list included, such as draft: no (false) or mode: 0755 (493), is a WARN that names the key and the value; quote it to keep the text. The date of verified: is text of the form YYYY-MM-DD.

It prints one line per finding, <SEVERITY> [<check>] <file>[:<line>] — <message>, then the counts by check and the totals. --quiet prints no WARN line, and the ERROR lines only when there is one. The hook pre-commit-wiki (see Git hooks and workflows) runs it with --quiet before each commit in a prose repository.

Exit status: 0 when there is no ERROR, and with --strict no WARN; 1 on a finding at the failing level; 2 for a TREE that is no directory, and when the Python environment lacks PyYAML.

Where to run it: anywhere; it changes nothing.

wiki-lint-hook ​

text
harness wiki-lint-hook [--apply]

Copies githooks/pre-commit-wiki as .githooks/pre-commit into each prose repository of the research tree (Knowledge, Environment, Tasks, Library), and sets core.hooksPath. It skips a name that is not a git repository.

Exit status and where to run it: as githooks.

workflows ​

text
harness workflows [--apply]

Copies the canonical files of githooks/workflows/ into each package and experiment that has a Project.toml: CI.yml, dependabot.yml and codecov.yml into each, TagBot.yml into each package, and Documenter.yml where a tracked docs/make.jl exists. A repository that the profile keys docs_exceptions or docs_additions name keeps its own documentation workflow. It deletes the retired CompatHelper.yml.

Exit status: 0 when every file is current, or after --apply; 1 when the dry run has a change; 2 when a template is missing.

Where to run it: anywhere; it writes tracked files only. Commit each file by name.

ci-protection ​

text
harness ci-protection [--apply] [--remove-classic]

Protects the default branch of each repository that has a CI.yml with a GitHub ruleset named main, which requires the status checks of the canonical CI.yml. Then it protects the Harness itself, the checkout that runs the command, with the same ruleset; there the required checks are the jobs of .github/workflows/test.yml that report on every pull request and are not warnings. It also turns on auto-merge and the deletion of merged branches. It reads the current state first and writes only what differs. It refuses to run unless verify-workflows.jl passes: a required check that never reports leaves every pull request pending.

optionmeaning
--applywrite through gh api; without it, print what would change
--remove-classicdelete classic branch protection where the ruleset passes every check

Run the ruleset first and --remove-classic afterwards, so that a branch is never left unprotected.

Exit status: 0 when nothing would change, or after --apply; 1 when the dry run has a change; 2 when the workflows are not canonical or a call fails.

Where to run it: in your own terminal. In a session's sandbox only the first gh call of a process succeeds.

format ​

text
harness format [--apply]

Runs JuliaFormatter over the tracked .jl files of the repositories below Packages/ and Experiments/. It skips a repository that has no .JuliaFormatter.toml, and each file on which JuliaFormatter crashes. The dry run counts the files that would change.

Exit status: 0 when no file would change, or after --apply; 1 when the dry run counts a file; 2 on a failure.

Files: the marker, the skip list and the log go to $FMT_OUT, or $TMPDIR if that is not set. The skip list that remains names the files that JuliaFormatter cannot format.

Where to run it: in your own terminal; it runs Julia over the whole tree.

The profile ​

render ​

text
harness render TEMPLATE

Prints TEMPLATE with each placeholder {key} replaced by its profile value. profile.md describes the placeholders.

Exit status: 0; 2 when a placeholder has no profile value.

get ​

text
harness get KEY

Prints the profile value of KEY, one line per item of a list, and develop of [setup] as true or false. A path that starts with ~ is printed with the home directory.

Exit status: 0; 2 when the profile does not load or has no KEY.

bash
harness get repository_roots

leaks ​

text
harness leaks [--commits FILE]

Searches the tracked files of the harness repository, and the templates rendered with the dummy profile, for the private strings of the profile's leak list and for the names of the research tree. It matches a whole word. The pre-push hook of the harness runs it.

optionmeaning
--commits FILEalso search the paths and lines that each commit of FILE adds, and its message; one SHA per line

Exit status: 0 with no hit, 1 on a hit, 2 when the profile does not load.

Settings and permissions ​

settings ​

text
harness settings surface [--settings F] [--days N] [--head N]
harness settings compare OLD NEW [--days N]
harness settings twins [--settings F] [--list allow|ask|deny]
harness settings selftest

Measures and checks the Claude Code settings against the Bash commands of the recent session transcripts in ~/.claude/projects/. Each subcommand of settings is read-only.

subcommanddoes
surfacecounts the segments of the recent Bash commands that reach a permission prompt under one settings file, by leading word. --days N reads the last N days (default 5); --head N prints the N most frequent prompting segments
comparethe same count for two settings files, side by side, with the difference
twinsprints the git -C * <sub> twin of each plain git rule: Bash(git status *) does not match git -C /path status
selftestchecks the matcher against the recorded cases of Claude Code; run it after a Claude Code update. Exits 1 when a case fails

--settings F defaults to ~/.claude/settings.json.

Exit status: 0; 1 for a failed selftest case; 2 when a settings file is missing, cannot be read, or is not a JSON object.

permissions ​

text
harness permissions [--apply]

Regenerates the permission block of adapters/opencode/opencode.jsonc and adapters/opencode/plugins/guard-paths.json in the harness repository from the deny and ask rules of the settings template. The dry run prints the report and the diff.

Exit status: 0 when the files are current, or after --apply; 1 when the dry run has a diff.

Where to run it: in the harness checkout; commit the two files with the change of the template.

Tests ​

test ​

text
harness test [NAME ...] [--changed BASE] [--list] [--jobs N] [--log FILE]

Runs the harness's own test cases as parts, each in a process of its own: the cases of each module and adapter, the groups of the hook probe and of doctor, and the contract sweep. The sweep runs the dry run of each subcommand on a fixture home directory and checks the contract above. It prints each part's lines and, last, <n> cases, <w> wrong. development.md describes the parts and the selection.

optionmeaning
NAME ...run these parts, with docs and sweep
--changed BASErun the parts that own a path of git diff --name-status BASE...HEAD, with docs and sweep; a path that no part owns selects every part
--listprint the selected part names, one per line, and run nothing
--jobs Nrun at most N parts at once; the default is half the CPUs
--log FILEwrite every line and a line part <name>: <n> cases, <w> wrong per part to FILE; print only the BAD lines and the summary

At most two runs share a machine; a third waits for one of them to end.

Exit status: 0 when every case passes, 1 when a case fails, 2 on a usage error: an unknown part, part names together with --changed, an empty BASE or one that git cannot resolve, --jobs below 1, a --log file whose directory does not exist, or a directory of the slot files that cannot be made or written.

mutate ​

text
harness mutate SPEC

Runs named mutants of the Python code of the harness. SPEC is a TOML file with one table per mutant, in the order in which they run:

toml
[MU1]
file = "lib/harness/changelog.py"   # a path relative to the harness checkout
anchor = 'DIRECTORY = "changelog.d"' # a text on exactly one line of file
old = 'HEADING = "## Unreleased"'    # a text that occurs exactly once after the end of that line
new = 'HEADING = "## Released"'      # the text that replaces it
tests = ["changelog"]                # the parts of harness test that should kill the mutant

The anchor tells two equal texts apart without a line number. mutate copies the checkout into a temporary directory: its tracked files and its untracked files that git does not ignore, with their working-tree bytes, as a git repository, because the parts docs and sweep read git ls-files. It writes nothing into the checkout or its .git, and removes the copy after the run, also after an error or an interrupt. The copy first runs harness test with the parts of every mutant, unmutated. Then each mutant is applied alone to the copy, harness test runs with its parts, and the file is reverted. It prints one line per mutant and, last, <n> mutants, <k> killed, <s> survived, <i> invalid:

linemeaning
<name> killed: <line>the first BAD line of the run. A run that fails with no BAD line, such as a crash at import, gives exit <status> with no BAD line: <its last line>
<name> killed (timeout): <line>the first BAD line is that of a part stopped after its timeout of 300 s
<name> SURVIVEDevery case of the mutant's parts passes
<name> invalid: <error>ast.parse refuses the mutated file; no test runs

A line of file ends at a newline only; a form feed does not end it. mutate stops before any mutant runs, and the message names the mutant, when a table lacks one of the five keys, has another key, or has a value of the wrong type; old is empty; file is not a file of the copy or is not UTF-8; anchor is on no line or on more than one; old does not occur exactly once after the anchor's line; tests is empty, or names a part that harness test --list does not name. It also stops when the spec cannot be read or holds no mutant, with a message that names the spec; when harness test --list of the copy fails, with its exit status; and when the unmutated copy fails the parts of the spec, with the first BAD line.

Exit status: 0 when every mutant is killed, 1 when a mutant survives or is invalid, 2 when it stops.

Where to run it: in the harness checkout. It runs harness test, so it takes as long as the parts that it names, once for the unmutated copy and once per valid mutant.

changelog ​

text
harness changelog [--apply | --check BASE]

Joins the changelog fragments of the harness checkout into its CHANGELOG.md. A fragment is a file changelog.d/*.md other than README.md, and holds the entry of one branch; changelog.d/README.md gives its name and its form. The dry run prints the ## Unreleased block that the fragments make: each fragment's text, with a final newline added where it lacks one, separated by one blank line. The newest fragment comes first, by the committer date of the commit that added it; a fragment that no commit holds comes before them, and fragments of the same date go by file name.

optionmeaning
--applyput the block at the top of the ## Unreleased section of CHANGELOG.md, above its entries, and delete the fragments. Without that heading, it inserts ## Unreleased above the first ## heading, or at the end
--check BASEread git diff --name-status BASE...HEAD, and fail when the branch changes a path below lib/, hooks/, adapters/, bin/, scripts/, githooks/, agents/, skills/ or rules/ and adds no fragment

A fragment that is empty, holds only whitespace, holds a carriage return or is not UTF-8 stops the dry run and --apply; the message names it, and no file changes.

Exit status: without --check, 0 when there is no fragment, or after --apply; 1 when the dry run prints a block; 2 on a bad fragment or a failure. With --check, 0 when a fragment is added or no code changes; 1 when code changes without a fragment, and the output names the first such path; 2 when BASE is empty or git cannot resolve it.

Where to run it: in the harness checkout. CI runs --check on each pull request.

skill-triggers ​

text
harness skill-triggers [--skill NAME ...] [--apply --model ALIAS]

Tests whether each skill loads on the queries of tests/skill-triggers/<skill>.toml, and stays unloaded on its near misses. The dry run prints the number of queries per skill and the number of sessions that --apply starts.

optionmeaning
--skill NAMEonly the query file of this skill; can repeat
--applyrun the sessions and write the report
--model ALIASthe model of the sessions; --apply needs it

--apply runs one claude -p session of 3 turns per query, at your cost, and writes the report to $RESEARCH_ROOT/.scratch/skill-triggers/<date>-<model>.md.

Exit status: the dry run exits 1; --apply exits 0 when every skill passes, else 1.

Where to run it: in your own terminal.

Environment ​

variablemeaning
RESEARCH_ROOTthe research tree; ~/Research if it is not set
RESEARCH_HARNESS_PROFILEthe profile, when --profile is not given
RESEARCH_HARNESS_MODELSthe model tables, when --models is not given
RESEARCH_HARNESS_REPOSITORIESthe repository table of clone and repos
RESEARCH_HARNESS_JULIAthe Julia environment of the scripts; ~/.local/share/research-harness/julia/ if it is not set
RESEARCH_HARNESS_PYTHONthe Python environment of the harness; ~/.local/share/research-harness/python/ if it is not set
OPENCODE_CONFIG_DIROpenCode's configuration directory; ~/.config/opencode if it is not set
PI_CODING_AGENT_DIRoh-my-pi's agent directory; ~/.omp/agent if it is not set
FMT_OUTthe directory of the marker, the skip list and the log of format
SANDBOX_RUNTIMEset by the Claude Code sandbox; doctor reports a refused check as SKIP

Files ​

filemeaning
~/.config/research-harness/profile.tomlthe private profile; examples/profile.toml shows every key
models.toml beside the profilethe model tables; examples/models.toml
providers.toml beside the profilethe model providers of OpenCode and oh-my-pi; examples/providers.toml
network.toml beside the profilethe overlay of the network lists of the settings; examples/network.toml
repositories.toml beside the profilethe repository table; examples/repositories.toml
~/.claude/.harness-install.jsonthe stamp of the last install --apply
~/.claude.jsonClaude Code's state, which trust writes

Exit status ​

statusmeaning
0nothing to do, the change is made, or the read-only subcommand finished
1the dry run has work to do; a check (doctor, leaks, test, mutate, settings selftest, skill-triggers) failed
2a usage error, a refusal or a failure, with one line on standard error

The Julia environment ​

The Julia scripts load their packages from Project.toml, which harness install --apply instantiates in ~/.local/share/research-harness/julia/; no caller needs --project.