Get Started
Reference

Runner contract

Exact process, marker, output, timeout, and result-submission behavior for model runners

A runner is a command thespider-agent runs with sh -c in the checkout. It gets a prompt file and must write findings on stdout. The agent posts the raw output; the server parses it.

Command binding

A [[model]] block names the MODEL (code) and the AGENT that runs it (agent). Agent-bound names one of the four built-in CLIs and lets TheSpider render the command:

[[model]]
code  = "xai/grok-4.6"
agent = "grok"
timeout_seconds = 1200

Freeform supplies the shell command directly, with or without an agent label. Any agent TheSpider cannot render requires this form:

[[model]]
code    = "MODEL_CODE"
agent   = "pi"                  # optional label; required to be paired with `command`
command = "COMMAND {prompt_path} {target_path} {model_code} {slice_id}"
timeout_seconds = 900

The containing .thespider/config.toml also stores project and profile. The profile selects the campaign methodology; model command bindings remain project-wide.

FieldRequiredBehavior
codeYesA canonical models.dev code, <lab>/<model> — or an mcp-* host-agent family code. Must match a code enabled for the project.
agentNoThe local CLI that runs the model. grok, claude, codex and opencode are built in, and command may then be omitted. Any other name is accepted and requires command.
harnessNoDeprecated alias of agent. Loads with a warning; an error if it disagrees with agent.
model_idNoVendor model string handed to the agent CLI. Derived from code (the <model> half; the whole provider/model code for OpenCode). Characters A-Z a-z 0-9 . _ / : - only.
effortNoThe reasoning effort, and the participant this binding claims. A built-in agent renders it into audit and agentic (refine fix) commands: Grok --reasoning-effort, Claude --effort, Codex -c 'model_reasoning_effort="X"', OpenCode --variant. Claude takes low, medium, high, xhigh, max; Grok none, minimal, low, medium, high, xhigh, max; anything else is an error for those two. Codex takes any value (setup and check-models warn outside none..max). OpenCode takes the provider's variant names. Beside a command, or on any other agent, it is declared: it still selects the participant, but it is not added to the command, so the command must set the effort itself. Same characters as model_id, at most 64, and no leading -. Ignored by agents before 0.8.0.
variantNoOpenCode's spelling of effort; must equal effort when both are set. An error for any other agent. Same rules as effort.
commandNoPOSIX-shell command template. Required unless agent is a built-in one. Always wins when both are present.
timeout_secondsNoLocal wall-clock timeout. Falls back to the built-in agent's default (Grok 1,200, Claude 900, Codex 1,200, OpenCode 1,200), then to the advisory lease timeout. Minimum effective value is one second.

Supported template variables are {prompt_path}, {target_path}, {model_code}, and {slice_id}. Values are shell-quoted. Unknown identifier-shaped variables fail substitution. The legacy standalone variables {thespider_dir} and {workspace_path} are not supported.

Validity

agentcommandVerdictWhat executes
absentpresent, non-blankValid (freeform)the command string (after template substitution)
a built-in agentabsentValid (agent-bound)that agent's rendered audit template
a built-in agentpresent, non-blankValid, warnsthe command string (after template substitution; the agent is not used to render)
any other agentpresent, non-blankValidthe command string; the agent name is an identity label
any other agentabsentError— TheSpider cannot render a command for an agent it does not know
absentabsentError—
anypresent but blankError— a blank command never falls back to the agent
grok, claude or codex with variantanyError— variant is OpenCode-only
opencode with effort and variant that differanyError— they are the same setting
claude or grok with an effort outside its listanyError— Claude would silently run at default effort
a built-in agent with effortpresent, non-blankValidthe command string; effort is declared, not applied
absent or any other agent, with effortpresent, non-blankValidthe command string; effort is declared, not applied
two blocks with the same code and the same effort (effort, else variant, else none)anyError— each participant has one binding
a code that is not <lab>/<model> or mcp-*anyError— with the models.dev URL and the expected shape
anyany, with model_id/effort/variant outside A-Z a-z 0-9 . _ / : - or empty, model_id over 200 characters, effort/variant over 64 characters or starting with -Error—

Precedence

When a block carries both agent and command, command always wins. The agent value is then used only to identify which CLI to probe for auth status, never to render a command. The combination is valid, so no command warns about it: setup prints it as a note, and status, models and check-models show it as the binding's own state: a custom command, with the agent probed only for install and login. THESPIDER_VERBOSE=1 also prints the note on run, daemon, gate, refine, status and models. An effort on such a block is declared, not applied: it still decides which participant's slices the binding claims, but TheSpider does not add it to the command. Put the effort flag in command yourself. check-models and setup show it as effort xhigh (declared; command not checked).

Provenance

code is what the server records. agent and model_id are local: which CLI actually ran. effort is the half of the participant the server records. A built-in agent with no command also runs with it; beside a command, or on any other agent, it is only declared, and the command must set the effort itself.

code can be a lab's own models.dev alias, such as deepseek/deepseek-flash. The server records the alias code. The catalog's alias_of (deepseek/deepseek-v4.1-flash) is advisory: TheSpider can't verify which model the vendor routes an alias to, so its slices and reports stay under the alias and are never moved to its target. Findings dedupe across models as usual.

Participants

A participant is a model code at one effort, (code, effort). A project enables participants, a campaign plans one slice per pass, bucket and enabled participant, and each slice is claimed only by a binding at that participant's effort. So anthropic/claude-opus-5-5 bound twice, once with effort = "low" and once with effort = "xhigh", audits every slice twice, and reports, the model dashboard and finding reports keep the two apart. Both pairs get the same prompt, apart from the slice id.

  • A binding with no effort (and no OpenCode variant) is the default participant: the runner's own default effort. TheSpider never claims it ran at any particular level.
  • The effort is what the binding was configured with. TheSpider does not verify what the provider actually ran.
  • Effort is never folded into code. The code is still the models.dev code, and the same string groups every effort of a model.
  • A server without effort participants (no model_effort in server_capabilities on GET /v1/me) only knows default participants. Against one, the agent claims with its bindings that have no effort, warns about the others, and refuses setup --effort.
  • An agent before 0.8.0 ignores a lone effort key, so it runs that binding at the default effort and claims only default slices, which is safe. Two blocks with one code are a parse error before 0.8.0, so upgrade every runner that shares the config before you add a second effort of a code.
  • From 0.8.0, an OpenCode variant is the effort, so a block that sets variant claims the participant code@variant, not the default one it claimed before. Enable that pair or remove variant when you upgrade (upgrade note).
  • Each participant has one binding in a config, so it runs through one agent at a time. A second concurrent agent needs a second enabled participant: another code, or the same code at another effort. The second one is audited separately at its own effort, and its slices are billed on their own.

Process contract

  • Working directory: target checkout root.
  • Shell: sh -c.
  • Stdin: closed.
  • Stdout: captured in full unless --redact is enabled.
  • Stderr: captured; only the final 4,000 characters are submitted.
  • Timeout: the agent stops the child process and reports timeout with no exit code.
  • Source tree: read locally by the runner; TheSpider does not upload it.

Because stdin is closed, every command must reach the prompt through the {prompt_path} file rather than expecting it on standard input. Each built-in agent template does this itself: Grok reads it with --prompt-file, Claude and Codex use a shell redirect from {prompt_path}, and OpenCode attaches it with -f after the positional instruction (OpenCode's -f is variadic).

The first-party agent itself exits 0 when every submitted result is valid coverage and 1 when one or more slices fail. A model process's nonzero exit is included in the result payload and stored as an artifact, but it is not valid coverage. The server still attempts to parse the marker so the warning is useful; the slice stays retryable.

Marker protocol

The claim response contains marker, and the rendered prompt states the corresponding output schema. The default marker is THESPIDER_FINDINGS_JSON.

A valid result block has this shape:

<THESPIDER_FINDINGS_JSON>
[
  {
    "title": "Concrete issue title",
    "severity": "high",
    "categories": ["authz"],
    "files": [{ "path": "src/http.ts", "line": 42 }],
    "evidence": "Exact evidence",
    "impact": "User-visible or security consequence",
    "recommendation": "Specific remediation"
  }
]
</THESPIDER_FINDINGS_JSON>

Rules:

  1. Use the marker from the prompt; do not hardcode the default when custom methodologies can change it.
  2. Include both opening and closing tags.
  3. The first non-whitespace character after the opening tag must be [.
  4. The enclosed value must be valid JSON.
  5. Write an empty array when there are no findings.
  6. Text outside the block is retained in the raw output artifact but is not a finding.

Security reports can include confidence, exploitScenario, cvssV4Score, cvssV4Vector, cweCodes, and evidenceBasis. Bug reports can include reproductionSteps, expectedBehavior, actualBehavior, rootCause, and testSuggestion. The server normalizes aliases and computes identity.

Hygiene and Pipeline reports carry two profile fields each. The model writes camelCase (snake_case is also accepted); finding reads return the snake_case key, and only for reports of that profile:

ProfileOutput fieldRead keyMeaning
HygieneusageEvidenceusage_evidenceHow non-use or staleness was established: the searches run, what they found, and the dynamic and external uses ruled out
HygieneremovalRiskremoval_riskWhat could break, and who outside the checkout could depend on it
PipelinecostDrivercost_driverWhere the time or compute goes, what triggers it, and whether it is on the merge or critical path
PipelineverificationPlanverification_planHow to confirm the saving from the team's own CI data

For Hygiene, recommendation is the removal plan and impact the cost of keeping the item. For Pipeline, impact is the structural saving (never minutes) and recommendation the change plus what preserves coverage. Neither profile emits exploitScenario, cweCodes or CVSS.

evidenceBasis (also accepted as evidence_basis) is optional: "demonstrated" when the cited evidence shows the problem concretely (an exploit or failure path for Security and Bugs, an item proven unused or stale for Hygiene, avoidable work visible in the pipeline files for Pipeline), "speculative" for a hardening observation, a removal that could not be fully confirmed, or a cost that depends on runtime behaviour. Any other value is ignored. It never changes a finding's identity. severity is matched case-insensitively against critical, high, medium (also moderate, med), low, info (also informational, information, note) and crit; anything else counts as no severity.

Truncation and size limits

BoundaryDefaultBehavior
Submitted stderr tail4,000 charactersOlder stderr is dropped by the agent.
Server artifact10 MiBControlled by MAX_ARTIFACT_BYTES; oversized stdout or stderr returns 413.
Agent --redact stdout1 MiBSecret-shaped strings are masked first, then the tail is dropped and a visible truncation marker is appended.

The result block should appear well before these limits. A truncation that removes the closing marker prevents parsing.

Claim and result fields

The host receives:

{
  "lease_id": 9001,
  "slice_id": 501,
  "model_code": "anthropic/claude-sonnet-5",
  "effort": null,
  "pass_code": "http",
  "bucket_code": "auth",
  "prompt": "FULL_RENDERED_PROMPT",
  "prompt_hash": "SHA256_HEX",
  "marker": "THESPIDER_FINDINGS_JSON",
  "timeout_seconds": 900,
  "lease_expires_at": "2026-07-22T12:00:00Z"
}

The host submits:

{
  "exit_code": 0,
  "duration_ms": 18422,
  "model_code": "anthropic/claude-sonnet-5",
  "client_version": "thespider-agent/0.1.0",
  "prompt_hash": "SHA256_HEX_FROM_CLAIM",
  "stdout": "RAW_STDOUT",
  "stderr_tail": "TAIL"
}

effort is the slice's participant effort (null for the default participant). The agent runs the lease with the binding of exactly that (model_code, effort) and never falls back to another effort of the same code; with no such binding the lease fails as an unbound code does. The result carries no effort: attribution comes from the slice.

prompt_hash protects prompt integrity. The result endpoint is idempotent on (lease_id, prompt_hash).

Submission dispositions

HTTP statusAgent textMeaning
200acceptedThis lease won. The runner exited 0 and the output contained a valid marked JSON array (including []). Reports were imported. The slice is valid coverage.
202accepted-as-artifact (superseded)Another lease already won, or the campaign was cancelled (warning campaign_cancelled); this output is retained but not imported, and completes no slice.
409rejected (prompt_hash mismatch)The echoed hash does not match the claimed prompt. The slice is unchanged.
413rejected (output too large)An artifact exceeds the configured size cap. The slice is unchanged.
422rejected (parse error) / rejected (runner exit)Artifact received and stored. The marker was missing, unclosed, malformed, or not an array, or the runner did not exit 0. The slice is failed and claimable again. Campaign completion does not increase.

An artifact can be received successfully while the audit execution fails. accepted means valid coverage, not merely that the POST arrived.

Repair historic invalid completions with thespider-agent repair --campaign ID. --dry-run lists valid / invalid / unverified without changing anything.

See custom runners for setup and external agents for implementing the lease protocol.

Copyright © 2026