Using Wardline with your coding agent¶
Wardline is built for the one-to-two-developer team that has handed real work to a coding agent and now wants a trust-boundary gate the agent can run itself — and reason about — without standing up an enterprise security program.
Why this fits an agent¶
An agent works best against a tool that is:
- Deterministic — the same code produces the same findings, so the agent can treat a verdict as ground truth rather than a roll of the dice.
- Dependency-free at the core — the analyzer is plain standard-library
Python (the CLI adds only
click,pyyaml, andjsonschema). Nothing to provision, no service to reach. The agent runs it the way it runspytest. - Fast and local — a scan is a process the agent spawns and reads, with a machine-readable exit code and a structured report.
That combination lets you wire Wardline into the loop the agent already runs: it edits code, runs the gate, reads the result, and corrects itself before it ever asks you to look.
If you have not installed Wardline yet, start with Getting Started.
One-command setup: wardline install¶
wardline install wires wardline into a project's agent context in one step:
- injects a small, hash-fenced block into
CLAUDE.mdandAGENTS.mdpointing the agent at the gate and the loop; - installs the
wardline-gateskill into.claude/skills/and.agents/skills/; - merges a
wardlineentry into.mcp.json(preserving any existing servers); - detects a Clarion taint store (
clariononPATHorWARDLINE_CLARION_URL) and a Filigree project (.filigree.conf), recording aclarion:/filigree:binding inwardline.yaml— live when a URL env var is set, otherwise a commented stanza for you to fill.
$ wardline install
wardline install:
CLAUDE.md: created
AGENTS.md: created
skill .claude/skills/wardline-gate: created
skill .agents/skills/wardline-gate: created
.mcp.json (wardline entry): created
clarion: detected (commented)
filigree: detected (commented)
It is idempotent (re-run to refresh after upgrading wardline) and non-interactive
(safe in CI). Opt out of any piece with --no-claude-md, --no-agents-md,
--no-skill, --no-mcp, or --no-bindings. There is no SessionStart hook —
freshness is enforced only when you re-run wardline install.
Once installed, the MCP server resolves the Clarion URL from wardline.yaml, so
the .mcp.json entry stays a bare wardline mcp --root . with no URL in its args.
Gate the agent's work with wardline scan¶
Wardline marks trust boundaries with two decorators from wardline.decorators:
@external_boundary (data arriving from outside the trust boundary —
untrusted) and @trusted (a producer that is supposed to receive validated data
only). When untrusted data reaches a trusted producer, Wardline raises
PY-WL-101 at ERROR.
Here is a self-contained example (handlers.py):
from wardline.decorators import external_boundary, trusted
@external_boundary
def read_request_body(req):
"""Untrusted: data arriving from the network."""
return req.body
@trusted
def store_record(req):
"""Trusted sink: this is supposed to receive validated data only."""
payload = read_request_body(req)
return payload
By default a scan reports but never fails — the gate is opt-in:
$ wardline scan .
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 new -> findings.jsonl
$ echo $?
0
Add --fail-on ERROR and the same scan becomes a gate — a non-suppressed defect
at or above the threshold drives a non-zero exit:
$ wardline scan . --fail-on ERROR
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 new -> findings.jsonl
$ echo $?
1
Why the agent can self-correct
Exit 1 is the gate tripping; exit 2 is a Wardline error (bad config,
unreadable path). The agent branches on the code. On a trip it reads the
structured report it just wrote — the line handlers.store_record declares
return trust INTEGRAL but actually returns EXTERNAL_RAW (less trusted) —
untrusted data reaches a trusted producer names the function, the file, and
the lines. That is enough for the agent to locate the leak and add a
validating boundary before handing the change back to you.
A pre-commit hook¶
To make the gate run on every commit, drop a .git/hooks/pre-commit script
(make it executable with chmod +x):
#!/usr/bin/env sh
# Block a commit if Wardline finds a new ERROR-or-worse defect.
# Write the findings file outside the working tree so the commit stays clean.
wardline scan . --fail-on ERROR --output /tmp/wardline-findings.jsonl
A scan always writes a findings file (default findings.jsonl in the scan
path), so point --output outside the tree — as above — or at a git-ignored
path; otherwise the hook litters every commit. The script's exit code becomes
the hook's exit code: a clean tree commits, a new defect aborts the commit with
the finding already on screen for the agent to act on.
Let the agent triage with wardline judge¶
The taint engine is intentionally conservative and will sometimes over-report.
wardline judge is an opt-in LLM pass that labels each active defect
TRUE_POSITIVE or FALSE_POSITIVE with a calibrated confidence. It costs
nothing by default — wardline scan never calls a model, and judge runs only
when you invoke it.
It also fails loud rather than guessing, which keeps an agent honest: with no
API key configured it stops with remediation guidance and exit 2, so the agent
never mistakes "couldn't triage" for "nothing to triage".
$ wardline judge .
error: WARDLINE_OPENROUTER_API_KEY is not set. `wardline judge` calls OpenRouter to triage findings. Export the key (`export WARDLINE_OPENROUTER_API_KEY=sk-or-...`) or place it in a .env in the scan root, then re-run.
With a key, judge triages cold and prints one line per verdict. Pass --write
to append FALSE_POSITIVE verdicts to .wardline/judged.yaml — but only those
at or above the confidence floor (judge.write_confidence_floor, default
0.5); a low-confidence FP is reported and held back rather than silently
suppressed. A subsequent wardline scan reads .wardline/judged.yaml and treats
those fingerprints as suppressed, so the gate stops tripping on triaged
false positives while still flagging anything new.
For an agent this closes the loop: scan flags a defect, judge classifies it,
and an above-floor false positive is recorded as an audited suppression rather
than left to nag every run. See the LLM triage judge guide
for the verdict format, the floor, and the judged.yaml record shape.
Hand off via SARIF¶
For handing findings to another tool — GitHub code scanning, a CI dashboard, or a sibling Loom tool — emit SARIF 2.1.0:
$ wardline scan . --format sarif --output results.sarif --fail-on ERROR
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 new -> results.sarif
The log is standard SARIF 2.1.0 with a wardline driver and one result per
finding (the defect alongside engine metric/fact entries), so it is not
Filigree-specific — any SARIF consumer can read it. --fail-on still gates while
the file is written, so the same command both publishes the report and blocks the
agent's change. See the Loom integration guide for the full
output matrix, including the native Filigree emitter.
Call Wardline as MCP tools¶
Wardline ships a native, dependency-free MCP server so an agent can call it as tools instead of shelling out. Launch it over stdio:
$ wardline mcp --root .
Tools: scan (structured findings + suppression summary + gate), explain_taint
(the tainted callee and originating boundary for one finding — call it right
after a scan and before editing), judge (opt-in, network), and the loud
suppression tools baseline_create / baseline_update / waiver_add (each
requires a reason). Resources expose the trust vocabulary, rule catalog, config,
and config schema. The wardline:loop prompt documents the intended
scan → explain → fix-at-the-boundary → rescan cycle.
With an opt-in Clarion taint store configured (wardline mcp --clarion-url
<URL>), explain_taint becomes a query when you pass the finding's qualname
as sink_qualname: a fresh fact is served from the store without re-scanning
the file. Pass chain: true (with an optional max_hops), again alongside
sink_qualname, to walk the full N-hop taint chain back to the originating
boundary. Without a store, or without
sink_qualname, explain_taint returns the single-hop SP8 explanation from a
local re-scan. Known cost: with a store configured, each scan additionally
builds taint facts (a blake3 hash per file) and POSTs them to Clarion — this is
fail-soft, but a real per-scan cost in the agent loop. See the
Clarion taint store guide for the full
opt-in, auth, and fail-soft details.
The server is stateless — no session state is carried between calls; the
read-only tools (scan, explain_taint) are pure functions of your code on disk
and your config, and the analysis core stays zero-dependency. Only judge
touches the network; the suppression tools (baseline_create / baseline_update
/ waiver_add, and judge with write) write to your project files as
requested.