target.yaml reference¶
A target file is how you point Mylonite at a custom MCP server (--target-file). One
YAML declares how to launch the server, which weakness classes it exposes, and how to
plant and verify attacks. mylonite scan --scaffold app.yaml scaffolds one for you; this
page is the full field reference. Source: mylonite.plugins._mcp.target_file.TargetFile.
Minimal¶
family: my-app
command: python
args: [my_server.py]
weakness_classes: [W2]
seed_arm:
tool: save_note
args_template: { body: "{payload}" }
Full, annotated¶
# --- how to launch the server ---------------------------------------------
family: my-app # short name used in report ids (not a bundled name)
command: python # the MCP server launch command
args: [my_server.py, --port, "0"]
env: { LOG_LEVEL: warning } # ADDED to a small allowlist, not merged into your full env — see below
scope: tenant-a # optional label; must match --authorize / {scope}
requires_scope: false # set true to require a non-empty scope
# --authorize must EQUAL this target's required value: the declared `scope`
# exactly (e.g. `--authorize tenant-a` above) if one is set, else `family`
# (e.g. `--authorize my-app`) if it isn't. This is derived from the target's
# own data, never from `requires_scope` — see `mylonite._authz`, SECURITY.md.
# --- the AI layer ----------------------------------------------------------
system_prompt: | # the target's system prompt (inline)...
You are a helpful assistant with access to the connected tools.
# system_prompt_file: prompts/system.txt # ...or read it from a file (one or the other)
primary_tools: [] # optional: narrow attack pattern selection to these tools
weakness_classes: [W1, W2, W3, W4] # which classes to test
# --- how to plant poisoned content (required for indirect-injection W2) ----
seed_arm:
tool: save_note # the tool that stores attacker-controllable content
args_template: { body: "{payload}" } # {payload} is the bare natural-language plant
id_key: id # JSON field in the result holding the new record's handle
# id_pattern: 'note_(\d+)' # ...or a regex to extract the id
# id_from: first_int # ...or "first integer in the result"
# --- how to confirm the damage materialised (recommended for custom targets)
effect_probe:
verify_tool: list_notes
verify_args_template: {}
expect_marker: "attacker@example.com" # a string that proves the effect landed
deferred_markers: ["queued for approval"] # markers that mean DEFENDED, not fired
# --- which tools implement which controls (for the synthetic guarded build) -
control_config:
egress_tools: [web_fetch] # W3: tools that reach the network
egress_url_param: url # the URL arg the allowlist guards
fetch_allowlist: [example.com] # hosts the egress control permits
consequential_tools: [send_email] # W4: high-impact actions to gate
read_tool_names: [read_note] # W2: tools whose results get quarantined
declared: [W2] # controls you've already implemented server-side
synthetic: [W3, W4] # controls NOT already declared — Mylonite synthesizes/tests these
# --- optional: a genuinely unguarded build + per-control server-layer toggles
vulnerable_launch: # how to launch a DELIBERATELY-unguarded variant
command: python
args: [my_server.py]
env: { SECURITY_MODE: "off" }
control_env: # per-control env that disables ONE server guard
W2: { SANITIZE_INPUT: "false" }
W3: { ALLOWLIST: "disabled" }
Remote targets (SSE / HTTP)¶
By default a target is launched over stdio (command/args spawn a subprocess). To
scan a remote MCP server, set transport and url instead — command becomes optional:
family: my-remote-app
transport: sse # stdio (default) | sse | http (streamable-HTTP)
url: https://app.example.com/mcp
headers: # optional; may carry auth
Authorization: Bearer my-token
weakness_classes: [W2, W4]
seed_arm: { tool: save_note, args_template: { body: "{payload}" } }
urlis required forsse/httpand rejected forstdio.headersare passed to the transport but never logged and never shown in the target descriptor (only the host appears). A target file keeps tokens out of shell history.command/args/envand the server-layervulnerable_launch/control_envtoggles do not apply to remote targets and are ignored.- Everything else (
seed_arm,effect_probe,weakness_classes,control_config) works exactly the same.
Field groups¶
- Launch (
family,command,args,env,scope,requires_scope) — how the stdio MCP server is started and labelled. For remote servers usetransport: sse|http+url+ optionalheadersinstead ofcommand/args. - AI layer (
system_prompt/system_prompt_file,primary_tools,weakness_classes) — what the agent is and what to test. Set at most one of the two prompt fields. seed_arm(SeedArmSpec) — how to plant untrusted content.{payload}must sit at a bare string leaf (e.g.body: "{payload}"), not nested inside serialized JSON. Theid_key/id_pattern/id_fromtell Mylonite how to capture the new record's handle so it can drive a read-back.effect_probe(EffectProbeSpec) — confirms the damage end-to-end, not just that a tool was called.expect_markerproves it fired;deferred_markersmean the action was defended (e.g. queued for approval), not a success.control_config(ControlConfig) — tells the synthetic guarded build which tools carry egress (W3), consequential actions (W4), and untrusted-data results to quarantine (read_tool_names, W2), the allowlist, which controls you'vedeclared, and whether tosynthetic-ally synthesize the rest.- Server-layer build (
vulnerable_launch,control_env) — optional: drive the differential against your own unguarded build and per-control env toggles, instead of the adapter-boundary shim. Use these when you can launch genuinely (un)guarded variants of the server. See Concepts and Security. framework— optional, free-form (e.g.langchain,crewai,llamaindex). Entirely a labelling hint: it names your agent framework in a structural recommendation's code sketch alongside the language Mylonite already infers fromcommand(python/uv/uvx/poetry→ Python,node/npx/bun/tsx→ TypeScript, else generic pseudocode). Never validated against a fixed list, and never used to fabricate that framework's actual hook/decorator syntax — Mylonite points you at where to wire a sketch in, not at invented API details it hasn't verified.
Windows SQLite footgun. If
envpoints at a SQLite DB by URL, note thatsqlite:////c/Users/...(4 slashes) andsqlite:///C:/Users/...(3 slashes) open different databases on Windows — a silent way to scan an empty DB and wrongly conclude the agent is clean. Prefer an absolute path and verify it opened.
envis an overlay, not your full environment. A stdio target's spawned process does not inherit Mylonite's own environment wholesale — Mylonite routinely spawns deliberately-vulnerable and third-party servers, and handing every one of them Mylonite's own provider API keys /GITHUB_TOKEN/ other credentials would be a real leak. The child gets a small, fixed allowlist of OS-plumbing variables (PATH,HOME,USERPROFILE,SYSTEMROOT,TEMP,TMP,TMPDIR,LANG,LC_ALL,PATHEXT,COMSPEC,APPDATA,LOCALAPPDATA) plus whatever you declare inenv:— nothing else. If your server needs some OTHER parent-env variable, declare it explicitly here. The most common case: annpx/uvx-launched target running behind a corporate TLS-inspecting proxy needs its proxy/CA variables declared explicitly too, e.g.env: { HTTPS_PROXY: "...", HTTP_PROXY: "...", NO_PROXY: "...", NODE_EXTRA_CA_CERTS: "...", SSL_CERT_FILE: "..." }— without them the launch can fail with a TLS/registry error that looks unrelated to Mylonite.
The boundary controls fail closed¶
The four boundary controls the adapter-boundary shim synthesizes (W1 description pinning, W2 information-flow control, W3 egress allowlist, W4 confirm-gate) each answer one question about a tool call: does this control apply to this tool? For W2/W3/W4, that answer is decided in this order:
- An explicit list in
control_config(read_tool_names/egress_tools/consequential_tools) — you said so, and this is always the final word for that tool name. - Structural evidence: W3 only — a call with a URL, a bare hostname, or an IP-literal argument is treated as egress regardless of what the tool is called.
- A name heuristic (substrings like
read/fetch/listfor W2,fetch/http/webfor W3 egress,send/delete/payfor W4 consequential actions) — a convenience, never the gate. - Otherwise: guarded. A tool that matches none of the above is still treated as in-scope for the control.
This means a W2/W3/W4 tool your target exposes that doesn't match any hint, and isn't
declared, is now guarded by default instead of silently passed through unguarded:
- W3 — an egress call with no destination Mylonite can identify is refused:
refused: ... no destination argument could be identified.
- W4 — an unconfirmed consequential call is staged, not blocked: the first
attempt is refused with a server-minted, single-use token
(confirmation_required: retry ... with confirm_token=...), and a retry
carrying that exact token is let through — a genuine two-step confirm flow,
not a permanent block.
- W2 — an unhinted (or simply undeclared) tool's non-error result taints the
session (labelled untrusted), same as an obvious read tool. With no
read_tool_names declared, this means EVERY tool's result taints the
session, not just retrieval-shaped ones. Once tainted, a call to a
consequential-or-egress-shaped tool is refused —
refused: ... is a sink and untrusted content is in scope this session —
until a fresh session starts; this is information-flow control (labelling
data, then gating the sink), not a text-marking envelope the model has to
respect on its own.
The first time this fires for a given tool name in a run, Mylonite logs a warning
(once per tool name) with the exact control_config snippet to paste to classify it
precisely — either to confirm it with the right list/argument name, or (by omitting it
from a non-empty declared list) to exempt it entirely. W1 has no comparable warning:
it sanitizes every tool description unconditionally, regardless of name, so there is
nothing to classify or exempt.
If your custom target has a read/egress/consequential tool with an unusual name (e.g.
materialise_record, dispatch_widget, relay_message), declare it up front so the
run doesn't spend a cycle discovering it:
control_config:
read_tool_names: [materialise_record]
egress_tools: [dispatch_widget]
egress_url_param: destination
consequential_tools: [relay_message]
Path containment¶
target.yaml is a shareable, PR-editable document — a teammate can mail you one, or
a pull request can edit the one already in your repo. Two fields resolve to a real
filesystem path, and both are contained, not just shape-checked:
system_prompt_fileresolves relative to the directory the target YAML itself lives in (itssource_dir), never the current working directory of whoever runsmylonite. A value like../../../../etc/passwd— or a symlink that points outside that directory — is refused before the file is ever opened; it cannot be used to read an arbitrary file off your disk. Seemylonite._paths.resolve_contained.mcp:filesystem:<scope>(the sandbox path handed to@modelcontextprotocol/server-filesystem) must be a real, existing, non-root directory:/,C:\, your home directory, and any path containing..are all rejected outright, and the directory must actually exist. SetMYLONITE_FS_SCOPE_ROOTto an absolute directory to additionally require every filesystem scope stay inside that root — an opt-in hard ceiling for CI/shared-runner environments that launch scans against target files they didn't author.
Both checks fail loud (PathEscapesBase / InvalidTargetScope) rather than silently
reading or sandboxing the wrong thing. See SECURITY.md for what a target.yaml you
received from someone else can and cannot do.