The harnesses
A harness is the agent CLI a task's session runs on: Claude Code (claude-code) or Codex
(codex). HarmFlow installs neither and signs in to neither: it starts the one you installed and
signed in to, under your own account. This page says what HarmFlow asks of each. The authority is the harness
adapters in harmflow-app,
daemon/internal/adapters/harness/.
Finding an installed harness
The daemon looks for each harness when it starts and again on POST /harnesses/refresh, and checks
the one a task needs again before each attempt. A harness is read in steps — not installed, installed, version
identified — and ends ready or not ready, with the reason. A path set by hand
(PUT /harnesses/{harnessId}) comes before the search.
Claude Code
- Where.
claudeon thePATH, then its usual places:~/.claude/local/,~/.local/bin/,/opt/homebrew/bin/,/usr/local/bin/,~/.npm-global/bin/, and~/.bun/bin/. - Which version.
claude --versionmust name Claude Code, at version 2.0 or later. A version newer than the ones HarmFlow was tested with runs, with a warning. - Which flags.
claude --helpmust list--print,--output-format,--permission-mode,--allowedTools, and--append-system-prompt. The efforts a task may ask for are the ones its--efforthelp lists. - Signed in.
claude auth statusmust not say it is signed out. A harness signed out is not ready: runclaudein a terminal and sign in, or start its sign-in from HarmFlow (POST /harnesses/claude-code/sign-in). A long-lived token saved through HarmFlow (PUT /harnesses/claude-code/token) reaches every check and session asCLAUDE_CODE_OAUTH_TOKEN.
Codex
- Where.
codexon thePATH, then the CLI inside the Codex app in/Applicationsor~/Applications, and/opt/homebrew/bin/,/usr/local/bin/,~/.local/bin/,~/.npm-global/bin/, and~/.bun/bin/. - Which version.
codex --versionmust name Codex. A version newer than the ones HarmFlow was tested with runs, with a warning. - Which flags.
codex exec --helpmust list--json,--sandbox,--cd, and--skip-git-repo-check, andcodex --helpmust listapp-server. - Signed in.
codex login statusmust not say it is signed out: runcodex login. Throughcodex app-server, HarmFlow also reads the account's sign-in method and plan, and warns when requirements an administrator set refuse the approval policy or the sandbox modes HarmFlow uses. - Models and efforts. Read from the models cache in Codex's home folder; when it cannot be read, a task's model and effort go unchecked, with a warning.
What a session is given
- One process per attempt, started in the task's workspace, in a process group of its own.
- The prompt on standard input, never on the command line.
- A context note on what the execution is and where its files go, and, after a failover, what the attempt before it did (below).
- The environment. The daemon's own, without the variables of a harness session it may
itself be running in and without any
HARMFLOW_variable it inherited, plusHARMFLOW_EXECUTION_ID,HARMFLOW_TASK_ID,HARMFLOW_EXECUTION_DIR, andHARMFLOW_WORKSPACE. - The model and the effort of the task's harness profile, passed only when they are not
default.
Claude Code
claude -p --output-format stream-json --verbose \
--no-session-persistence \
--model <model> --effort <effort> \
--append-system-prompt <context note> \
--permission-mode <mode> --allowedTools <tools> \
--add-dir <execution folder>
- Sessions are not kept:
--no-session-persistencewhere the CLI has it. Claude Code does not takeephemeral: false. - Permissions.
workspace_writeruns with--permission-mode acceptEditsand an allowlist of the reading, editing, and shell tools.read_onlyruns with--permission-mode dontAskand only the reading tools, the shell and the editing tools disallowed.custompasses itsallowedToolsanddeniedToolsas--allowedToolsand--disallowedTools, and eachwritablePathsentry as--add-dir. A denied network disallows the web tools. - What Claude Code cannot enforce runs with a warning: its shell is not confined to the workspace, a denied network does not stop a shell command, and allow rules in your own Claude Code settings can widen a task's permissions.
- Agents and skills. HarmFlow's built-in skills reach the session as a plugin of that
session only (
--plugin-dir), and its built-in agents as--agents, each a subagent: the session runs as Claude Code's default agent. Your own agents and skills are the ones Claude Code loads itself, from its configuration and the workspace; HarmFlow checks that each one a task cites is there.
Codex
codex exec --json --skip-git-repo-check \
--cd <workspace> --ephemeral --color never \
--model <model> \
-c model_reasoning_effort="<effort>" \
-c approval_policy="never" \
--sandbox workspace-write \
-c sandbox_workspace_write.network_access=true \
--add-dir <execution folder> -
- Sessions are not kept:
--ephemeral, unless the profile setsephemeral: false. - Nothing waits for an approval:
approval_policy="never". - Permissions.
read_onlyruns in theread-onlysandbox, which blocks the network; until a real run proves that it also refuses the file changes Codex makes itself, the profile runs with a warning that it is not enforced. Every other profile runs in theworkspace-writesandbox, whose writes stay in the workspace and the added folders, with the network as the task sets it.custompasses eachwritablePathsentry as--add-dir; Codex has no per-tool lists, so itsallowedToolsanddeniedToolsrun with a warning that they are not enforced. - Agents and skills. The instructions on standard input are the context note, then a list of
the cited agents and a list of the cited skills — each with its description and the path of its file, to read
in full and follow — then the prompt. Your own agents come from Codex's home folder, and your skills are
the ones
codex app-serverlists for the workspace.
What HarmFlow reads back
- Claude Code prints one JSON object per line (
stream-json). HarmFlow reads the session id and the model from itsinitline, the messages, the tool calls and their results — the shell commands, and the files the editing tools changed — the rate-limit notices, and theresultline: the final text, whether it is an error, the cost, the token use per model, and the tool calls Claude Code refused. - Codex prints JSON lines (
--json). HarmFlow reads the thread id, the token use of each turn, the shell commands with their exit codes, the changed files, the final message, a failed turn, and the notices that a setting was overridden by an administrator's requirements. The cost is priced from the token use. - Each attempt ends in one class: none (it succeeded),
quota_exhausted,rate_limited,harness_unavailable(signed out included),network,timeout,cancelled,task_error, orunknown. HarmFlow reads the class from what the harness reported, and anything it cannot tell apart is atask_error, never a quota: only a clear signal makes a task fail over. - Extra usage. Claude Code says when a session turns to usage billed beyond its plan; when the task does not allow extra usage, HarmFlow ends that attempt. Codex does not say it, so on Codex the setting runs with a warning that it is not enforced.
How an attempt is stopped
On a cancel, a timeout, or the end of the harness itself, HarmFlow signals the harness's process group and
every process the attempt started with SIGTERM, waits up to the task's
execution.cancelGraceSeconds, then ends what is left with SIGKILL. A process the
attempt left running is found by its group, its descent, and the execution id in its environment, and the
execution records what it ended. After a restart of the daemon, the processes an attempt left are found and
ended too.
How a task fails over
A task names its harnesses in spec.harness of its task
file: preferred, then fallback.candidates in order. A candidate that lacks one of
the task's agents or skills is left out of the chain, with an invalid_fallback warning.
- Before each attempt, HarmFlow checks the harness again: one not ready counts as
harness_unavailable, and one whose quota is exhausted, when the task does not allow extra usage, counts asquota_exhaustedwithout starting. quota_exhaustedandharness_unavailableswitch to the next harness of the chain, whenfallback.onlists the class, a next harness exists, and the execution has switched fewer times thanfallback.maxSwitches. With no harness left, the execution fails withharness.no_alternative.rate_limitednever switches. The attempt retries on the same harness, up toexecution.rateLimit.maxRetriestimes, waiting for the reset the harness reported or else forinitialWaitSeconds, doubling up tomaxWaitSeconds.- Every other class ends the execution:
network,timeout,task_error, andunknownfail it, andcancelledcancels it. Oneexecution.timeoutMinutescovers every attempt. - The next harness starts a new session. Its context note names the harness and model before
it, how that attempt ended, its last operation, and its last message, with the files changed in the workspace
so far; and the execution's
context.jsoncarries each work item's branch, head commit, and last test result. The note asks the session to check what the attempt before it already did before it commits, pushes, or opens a pull request again. The switch is aharness.switchedevent.