Runner contract
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.
| Field | Required | Behavior |
|---|---|---|
code | Yes | A canonical models.dev code, <lab>/<model> — or an mcp-* host-agent family code. Must match a code enabled for the project. |
agent | No | The 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. |
harness | No | Deprecated alias of agent. Loads with a warning; an error if it disagrees with agent. |
model_id | No | Vendor 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. |
effort | No | The 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. |
variant | No | OpenCode's spelling of effort; must equal effort when both are set. An error for any other agent. Same rules as effort. |
command | No | POSIX-shell command template. Required unless agent is a built-in one. Always wins when both are present. |
timeout_seconds | No | Local 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
agent | command | Verdict | What executes |
|---|---|---|---|
| absent | present, non-blank | Valid (freeform) | the command string (after template substitution) |
| a built-in agent | absent | Valid (agent-bound) | that agent's rendered audit template |
| a built-in agent | present, non-blank | Valid, warns | the command string (after template substitution; the agent is not used to render) |
| any other agent | present, non-blank | Valid | the command string; the agent name is an identity label |
| any other agent | absent | Error | — TheSpider cannot render a command for an agent it does not know |
| absent | absent | Error | — |
| any | present but blank | Error | — a blank command never falls back to the agent |
grok, claude or codex with variant | any | Error | — variant is OpenCode-only |
opencode with effort and variant that differ | any | Error | — they are the same setting |
claude or grok with an effort outside its list | any | Error | — Claude would silently run at default effort |
a built-in agent with effort | present, non-blank | Valid | the command string; effort is declared, not applied |
absent or any other agent, with effort | present, non-blank | Valid | the command string; effort is declared, not applied |
two blocks with the same code and the same effort (effort, else variant, else none) | any | Error | — each participant has one binding |
a code that is not <lab>/<model> or mcp-* | any | Error | — with the models.dev URL and the expected shape |
| any | any, 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 OpenCodevariant) 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_effortinserver_capabilitiesonGET /v1/me) only knows default participants. Against one, the agent claims with its bindings that have no effort, warns about the others, and refusessetup --effort. - An agent before 0.8.0 ignores a lone
effortkey, 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
variantis the effort, so a block that setsvariantclaims the participantcode@variant, not the default one it claimed before. Enable that pair or removevariantwhen 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
--redactis enabled. - Stderr: captured; only the final 4,000 characters are submitted.
- Timeout: the agent stops the child process and reports
timeoutwith 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:
- Use the marker from the prompt; do not hardcode the default when custom methodologies can change it.
- Include both opening and closing tags.
- The first non-whitespace character after the opening tag must be
[. - The enclosed value must be valid JSON.
- Write an empty array when there are no findings.
- 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:
| Profile | Output field | Read key | Meaning |
|---|---|---|---|
| Hygiene | usageEvidence | usage_evidence | How non-use or staleness was established: the searches run, what they found, and the dynamic and external uses ruled out |
| Hygiene | removalRisk | removal_risk | What could break, and who outside the checkout could depend on it |
| Pipeline | costDriver | cost_driver | Where the time or compute goes, what triggers it, and whether it is on the merge or critical path |
| Pipeline | verificationPlan | verification_plan | How 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
| Boundary | Default | Behavior |
|---|---|---|
| Submitted stderr tail | 4,000 characters | Older stderr is dropped by the agent. |
| Server artifact | 10 MiB | Controlled by MAX_ARTIFACT_BYTES; oversized stdout or stderr returns 413. |
Agent --redact stdout | 1 MiB | Secret-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 status | Agent text | Meaning |
|---|---|---|
200 | accepted | This lease won. The runner exited 0 and the output contained a valid marked JSON array (including []). Reports were imported. The slice is valid coverage. |
202 | accepted-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. |
409 | rejected (prompt_hash mismatch) | The echoed hash does not match the claimed prompt. The slice is unchanged. |
413 | rejected (output too large) | An artifact exceeds the configured size cap. The slice is unchanged. |
422 | rejected (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.