Skip to content

Agent harnesses

A harness is one agent runtime, described as data — a fact about a CLI, not code. An agent-kind hook in registry.yaml names one by id in its harness: field:

on.spec.requested: { kind: agent, harness: claude, skill: spec, artifact: spec }

harness defaults to claude when omitted. Kraft ships three:

id Binary Notable gaps
claude claude Full capability set.
codex codex exec No deny_tools, allowed_tools, approval_channel, autocompact, or rate_limit_signal — a binding naming one of those is rejected at load.
gemini gemini No out-of-band context channel (context goes in-band via the prompt), no effort, no resume at all (Gemini's --resume takes an index or "latest", not a session id, so the capability isn't declared).

Capabilities, not flags

A hook's YAML never names a harness's actual CLI flags. It asks for a capabilityprompt, context, model, effort, permission_mode, deny_tools, allowed_tools, approval_channel, resume, autocompact, structured_log, usage, rate_limit_signal — and each harness's own YAML (src/kraft/harnesses/*.yaml in the package) maps that capability onto whatever its CLI actually calls it. permission_mode is --permission-mode acceptEdits|auto|... for Claude, -s read-only|workspace-write|... for Codex, --approval-mode default|yolo|... for Gemini — one Kraft-side name, three different flags.

Three capabilities are required — prompt, context, usage — since no agent dispatch can be built without them. Two are non-invocable —usage, rate_limit_signal — they describe what Kraft reads back out of a session (from its structured log or a result file), not an argv it constructs.

Some harnesses declare values: on a capability — a closed vocabulary the CLI itself would reject (Codex's effort is minimal, low, medium, high, Claude's is low, medium, high, xhigh, max) — checked at load time, and always: — a value Kraft forces regardless of what a binding asks for (Gemini's permission_mode is always yolo: the disposable worktree is the real safety boundary, not the approval mode, and a headless worker has nobody to answer an approval prompt anyway).

Overriding or adding one

An operator drops a same-named YAML file into $KRAFT_HOME/templates/harnesses/ to override a shipped harness (say, claude's --model allowlist) or add a new one entirely. Not seeded by the usual templates/ copy-on-first-run — a seeded copy would freeze at whichever version was installed when Kraft first ran, so this directory only exists once someone has deliberately put something in it.

kraft admin doctor runs one PATH check per harness the live registry actually names (not every bundled one — an install with no codex binding anywhere isn't told to go install codex), plus a failure row for any harness file that failed to load at all.

Adding one

A new harness is a YAML file at $KRAFT_HOME/templates/harnesses/<id>.yaml (the same override directory as above) — no Python change, no Kraft release. Required top level:

id: mytool          # must match the filename's stem
kind: cli            # the only kind implemented; a second kind gets its own adapter
command: [mytool]    # argv prefix
capabilities:
  prompt:   { cli: ["-p", "{value}"] }
  context:  { channel: prompt }   # or: { channel: system_prompt, cli: [...] }
  usage:    { source: result_file }   # or: { source: envelope, reader: <a Python parser's name> }

prompt, context, and usage are required — nothing can dispatch without them. Every other capability (model, effort, permission_mode, deny_tools, allowed_tools, approval_channel, resume, autocompact, structured_log, rate_limit_signal) is optional: omit what the CLI can't do, and a binding naming it is rejected at load, pointing at this file.

A capability needs a cli: argv fragment unless it's usage/ rate_limit_signal (read back out, not invoked) or context with channel: prompt (folded into the prompt text itself, not a separate flag). {value} and {csv} are the whole placeholder language — a comma-joined list for {csv}, a single substituted string for {value}. Needing a third kind of substitution is a sign the harness belongs in code, not YAML.

Two more fields keep a binding from doing something the CLI would reject: values: (a list of re.fullmatch patterns — a value outside them fails at load, not at launch) and always: (a value Kraft forces regardless of what a binding asks for, checked against values: too, since a default the CLI itself would reject is worse than no default). resume can bind via: command_resume instead of cli:, when a resume needs its own command prefix rather than a trailing flag (see codex.yaml's command_resume: [codex, exec, resume, "{value}"]).

Validate with kraft admin doctor — it loads every harness a live binding names and reports a PATH check for each, plus the load error for any file that failed outright.