Configuration
Every key sealmark.yaml accepts today.
Generated from the CLI's own description of its config (methodology 0.3-draft, CLI 0.3.0). Planned keys are not listed here. Locked keys are part of the standard methodology: changing one makes the run CUSTOM. Deprecated keys are still read, with a warning, and are shown muted.
Keys
| Key | Type | Default | Since | Locked | Meaning |
|---|---|---|---|---|---|
schema_version | integer | 1 | 0.2-draft | no | Config file layout version. Required in project files (a missing field is read as 1 with a warning). `sealmark config migrate` upgrades older files. |
name | string | my-server | 0.1-draft | no | Display name of the server under test. When not set, it is taken from the server's command (or the host of its url); `my-server` is only the fallback. |
version | string | 0.0.0 | 0.1-draft | no | Version of the server under test as you know it (reported next to the version the server announces). |
profile | string | standard | 0.2-draft | no | Profile to start from: standard, quick, ci, strict, paranoid, or a name defined under `profiles`. |
profiles | object | none | 0.2-draft | no | User-defined profiles: a name maps to a partial config with an optional `extends`. Project-level settings such as server cannot appear inside a profile. |
server.command | string | none | 0.1-draft | no | Program that starts the MCP server. Alone it is a stdio server; with url, Sealmark starts it, waits for url to answer, tests it and stops it. |
server.args | string[] | none | 0.1-draft | no | Arguments for command. Placeholders such as {dsn}, {sandbox} are filled by the harness. Replaces, never appends. |
server.env | stringMap | none | 0.1-draft | no | Environment variables for the server. Values under secret-looking names are redacted in reports. |
server.url | string | none | 0.1-draft | no | URL of a Streamable HTTP server. Alone it must already be running; with command, Sealmark starts it and waits for this URL. |
server.start_timeout_sec | integer | 60 | 0.2-draft | no | Seconds to wait for a server started with command + url to answer on url (default 60). A server that does not answer in time fails the run with its last output. |
server.headers | stringMap | none | 0.1-draft | no | HTTP headers sent to the server (url transport). Values under secret-looking names are redacted. |
server.rootless | boolean | false | 0.3-draft | no | The server takes no root argument and works in its own working directory: the fs pack treats that directory ({sandbox}) as the allowed root, so every path probe runs and a server that reads or writes any path fails the escape checks. Use it for a path server you cannot give a {sandbox} argument. Not locked: it only adds coverage, so a STANDARD run stays STANDARD. |
server.files | list | none | 0.2-draft | no | Files written inside the sandbox before the server starts, as { path, content }. path must start with {sandbox} or {sandbox_home}; content may use harness placeholders and ${VAR}, never {dsn}. File only. |
servers.<name>.* | map of server | none | 0.2-draft | no | Several servers in one file. Each entry takes the same keys as server: command, args, env, url, start_timeout_sec, headers and files, with the same meaning. Use either server or servers, never both; run.server picks the entry. |
sql_tools | list | none | 0.1-draft | no | Tools that take a SQL string, as { tool, arg } (optional role: reader|writer|unknown, shape: string|array, args: { <parameter>: <string|number|boolean> } for the tool's other required parameters, which a placeholder cannot satisfy). Auto-detected when omitted. A list adds tools and corrects arguments; it never hides a detected tool (use tools.exclude for that), and a detected writer stays a writer. |
run.server | string | none | 0.2-draft | no | Which entry of `servers` to run: a name or `all`. Empty runs every declared server. |
packs | string[] | ["core","db","sqlite","fs","net","git","http","effects"] | 0.1-draft | no | Packs to run. Removing a standard pack makes the run CUSTOM; adding one (exec, custom) does not. exec only runs with --active. |
timeout_probe_sec | number | 10 | 0.1-draft | locked | Seconds the cap and timeout probes wait before a call counts as running too long (flag --timeout-probe). |
checks.<id>.enabled and .severity | boolean, enum | true, none | see the check | locked (not for candidate checks: refusal-quality, instructions-hygiene, db-privilege, http-session-binding, scope-escape, sql-guard-statements, sql-guard-references, sql-guard-rowcap, upstream-credential, upstream-credential-echo, log-secret, upstream-error-leak, upstream-slow, upstream-token-passthrough, oauth-token-forgery, oauth-claims, oauth-discovery, oauth-scope) | Both exist for every check, so they are listed once here, and not repeated on each check page. enabled: false reports the check as n/a ("disabled by config") so it stays visible. severity is critical, scored or advisory and overrides how the check counts; none keeps the check's own severity, and advisory removes it from the score. |
checks.mutating-sql.corpus.add | list | [] | 0.1-draft | locked | Extra probes for mutating-sql, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.mutating-sql.corpus.remove | string[] | [] | 0.1-draft | locked | Probe ids or exact strings to drop from the mutating-sql corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.sqlite-mutating-sql.corpus.add | list | [] | 0.2-draft | locked | Extra probes for sqlite-mutating-sql, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.sqlite-mutating-sql.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the sqlite-mutating-sql corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.sqlite-file-read.corpus.add | list | [] | 0.2-draft | locked | Extra probes for sqlite-file-read, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.sqlite-file-read.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the sqlite-file-read corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.sqlite-file-write.corpus.add | list | [] | 0.2-draft | locked | Extra probes for sqlite-file-write, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.sqlite-file-write.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the sqlite-file-write corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.sqlite-extension-load.corpus.add | list | [] | 0.2-draft | locked | Extra probes for sqlite-extension-load, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.sqlite-extension-load.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the sqlite-extension-load corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.fs-read-escape.corpus.add | list | [] | 0.2-draft | locked | Extra probes for fs-read-escape, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.fs-read-escape.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the fs-read-escape corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.fs-write-escape.corpus.add | list | [] | 0.2-draft | locked | Extra probes for fs-write-escape, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.fs-write-escape.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the fs-write-escape corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.fs-link-escape.corpus.add | list | [] | 0.2-draft | locked | Extra probes for fs-link-escape, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.fs-link-escape.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the fs-link-escape corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.net-loopback-ssrf.corpus.add | list | [] | 0.2-draft | locked | Extra probes for net-loopback-ssrf, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.net-loopback-ssrf.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the net-loopback-ssrf corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.net-scheme-allowlist.corpus.add | list | [] | 0.2-draft | locked | Extra probes for net-scheme-allowlist, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.net-scheme-allowlist.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the net-scheme-allowlist corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.git-repo-scope.corpus.add | list | [] | 0.2-draft | locked | Extra probes for git-repo-scope, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.git-repo-scope.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the git-repo-scope corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.git-arg-injection.corpus.add | list | [] | 0.2-draft | locked | Extra probes for git-arg-injection, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.git-arg-injection.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the git-arg-injection corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.exec-injection.corpus.add | list | [] | 0.2-draft | locked | Extra probes for exec-injection, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.exec-injection.corpus.remove | string[] | [] | 0.2-draft | locked | Probe ids or exact strings to drop from the exec-injection corpus. Appends across layers. Any removal makes the run CUSTOM. |
checks.db-rowcap.row_cap | integer | 10000 | 0.2-draft | locked | Rows a single response may return before db-rowcap fails (standard 10000). |
checks.db-timeout.expected_sec | number | none | 0.2-draft | locked | Seconds a runaway query may run before db-timeout fails. null uses timeout_probe_sec. |
checks.db-timeout.slow_sql | string | none | 0.2-draft | locked | A SELECT that runs for a long time, used instead of pg_sleep when a server refuses pg_sleep. Must be a SELECT. null keeps the built-in probe. |
checks.sqlite-rowcap.row_cap | integer | 10000 | 0.2-draft | locked | Rows a single response may return before sqlite-rowcap fails. |
checks.sqlite-timeout.expected_sec | number | none | 0.2-draft | locked | Seconds a runaway query may run before sqlite-timeout fails. null uses timeout_probe_sec. |
checks.robust-input.hang_ms | integer | none | 0.1-draft | locked | Milliseconds a call may hang on bad input before robust-input fails. null derives max(2000, timeout_probe_sec * 1000). |
checks.robust-input.max_tools | integer | 25 | 0.1-draft | locked | Most tools robust-input will probe. |
checks.fs-size-cap.file_mib | integer | 64 | 0.2-draft | locked | Size in MiB of the file fs-size-cap asks the server to read. |
checks.fs-size-cap.pass_mib | integer | 16 | 0.2-draft | locked | Largest response in MiB that still passes fs-size-cap (standard 16). |
checks.cap-timeout.enabled | boolean | none | 0.1-draft | no | deprecated. Deprecated alias: cap-timeout was split into db-rowcap and db-timeout. Use checks.db-rowcap.enabled and checks.db-timeout.enabled. |
checks.cap-timeout.severity | enum | none | 0.1-draft | no | deprecated. Deprecated alias: cap-timeout was split into db-rowcap and db-timeout. Use checks.db-rowcap.severity and checks.db-timeout.severity. |
checks.cap-timeout.timeout_sec | number | none | 0.1-draft | no | deprecated. Deprecated alias: cap-timeout was split into db-rowcap and db-timeout. Use checks.db-timeout.expected_sec. |
checks.cap-timeout.row_cap | integer | none | 0.1-draft | no | deprecated. Deprecated alias: cap-timeout was split into db-rowcap and db-timeout. Use checks.db-rowcap.row_cap. |
sandbox.enabled | boolean | true | 0.2-draft | locked | Run packs that need a harness-owned sandbox. false (--no-sandbox) inherits the environment, disables fs, git, exec and effects, and makes the run CUSTOM. |
sandbox.dir | string | none | 0.2-draft | no | Parent directory for the disposable sandbox. null uses the OS temp directory. |
sandbox.keep | boolean | false | 0.2-draft | no | Keep the sandbox after the run (--keep-sandbox). |
sandbox.scrub_env | boolean | true | 0.2-draft | locked | Start the server with a built-from-scratch environment instead of inheriting yours. |
sandbox.keep_env | string[] | [] | 0.2-draft | locked | Variable names copied from your environment into the scrubbed one (for example HTTP_PROXY). |
sandbox.pack_timeout_sec | number | 600 | 0.2-draft | locked | Seconds before one pack is abandoned and reported inconclusive (--pack-timeout). |
sandbox.max_mib | integer | 1024 | 0.2-draft | locked | Size cap of the sandbox directory in MiB; a probe that exceeds it is aborted and fails. |
tools.include | string[] | ["*"] | 0.2-draft | locked | Glob patterns of tool names to probe, evaluated before exclude. A higher layer replaces the list (it is not mergeable: a catch-all default could never be narrowed by appending). |
tools.exclude | string[] | [] | 0.2-draft | locked | Glob patterns of tool names never to probe (listed under not covered). Appends across layers. |
tools.overrides | object | {} | 0.2-draft | locked | Per-tool classifier overrides: { tool: { class, params } }. An override always wins and is printed. |
timeouts.connect_ms | integer | 60000 | 0.2-draft | no | Milliseconds to wait for the server to start and answer initialize (--connect-timeout, in seconds). |
timeouts.call_ms | integer | 30000 | 0.2-draft | locked | Milliseconds before an ordinary tool call counts as timed out. |
timeouts.probe_ms | integer | 10000 | 0.2-draft | locked | Milliseconds before a verdict probe counts as timed out. |
retries.connect | integer | none | 0.2-draft | no | deprecated. No longer used and ignored: nothing retries a server start. |
retries.call | integer | none | 0.2-draft | no | deprecated. No longer used and ignored: nothing retries a control call. |
concurrency.servers | integer | none | 0.2-draft | no | deprecated. No longer used and ignored: servers run one after another. |
concurrency.packs | integer | none | 0.2-draft | no | deprecated. No longer used and ignored: packs run one after another (they share observers). |
net.control_url | string | none | 0.2-draft | locked | A URL the harness may fetch through the server as a control, so a no-hit, no-refusal result can pass instead of staying inconclusive. |
net.allow_dns | boolean | false | 0.2-draft | locked | Allow probes that need DNS resolution (127.0.0.1.nip.io). |
sqlite.engine | enum | none | 0.2-draft | no | Force the embedded-database engine instead of detecting it from {sqlite_path} / {duckdb_path}. Setting it makes the run CUSTOM (it can switch the sqlite pack off). |
db.embedded | boolean | none | 0.1-draft | no | false (--no-embedded) disables the embedded Postgres. null starts it when the db pack needs it. |
db.pg | string | none | 0.1-draft | no | Superuser DSN of an existing Postgres to seed instead of the embedded one (--pg). Needs --yes. Treated as a secret. |
upstream.kind | enum | none | 0.3-draft | no | Kind of the harness-owned upstream stub the server's backend is pointed at through {upstream}. http starts a loopback stub that records every request the server forwards (flag --upstream). null starts none. The stub needs a server Sealmark starts, and the server must use {upstream} in command, args, env, url, headers or files. |
upstream.responses | list | [] | 0.3-draft | no | Scripted replies of the upstream stub, tried in order: { id?, match?: { method?, path?, body?, header? }, reply }. match values are regular expressions (method is exact); an entry without match answers everything. reply needs exactly one of body, json or bytes, plus optional status, headers and delay_ms. {upstream_token} in a reply body becomes a fresh marker per occurrence. |
upstream.default_reply | object | {"status":200,"json":{}} | 0.3-draft | no | What the upstream stub answers when no `responses` entry matches. |
upstream.credential_header | string | authorization | 0.3-draft | no | Header the upstream-credential check reads the forwarded credential from (compared case-insensitively). |
upstream.max_body_kib | integer | 1024 | 0.3-draft | no | Largest request body in KiB the upstream stub records; the rest is cut and marked truncated. |
identities.a | string | none | 0.3-draft | no | Token the harness sends as caller A wherever the server config says {bearer} or {bearer:a}. null mints one per run. Set it when the server validates tokens it did not issue. ${VAR} is read from the environment. Treated as a secret: redacted in config show and in reports (flag --identity-a). |
identities.b | string | none | 0.3-draft | no | Token the harness sends as caller B ({bearer:b}). null mints one per run. Must differ from identities.a. Treated as a secret (flag --identity-b). |
scope.tools | list | [] | 0.3-draft | no | Tools that take an identifier the server is meant to restrict (a table, dataset, path), as { tool, param }: the scope pack sends out-of-scope forms of the identifier through each. The tool keeps its classification. |
scope.allow | string[] | [] | 0.3-draft | no | Identifiers the server may serve. An entry that ends with scope.separator is a prefix (sales.public.); any other entry is exact. Needed when scope.tools is not empty. |
scope.deny | string[] | [] | 0.3-draft | no | Identifiers the server must refuse even inside scope.allow (same syntax; deny wins). |
scope.separator | string | . | 0.3-draft | no | The one character that separates the parts of an identifier (and ends a prefix entry). |
scope.case | enum | insensitive | 0.3-draft | no | Whether the server compares identifiers case-insensitively (the default) or case-sensitively. An upper-case spelling of an allowed name that is refused under insensitive is noted, never failed. |
checks.sql-guard-rowcap.row_cap | integer | 10000 | 0.3-draft | no | Largest LIMIT (or FETCH FIRST / TOP) the statement the server forwards may carry before sql-guard-rowcap fails. A candidate setting: unlocked. |
checks.sql-guard-statements.control_sql | string | none | 0.3-draft | no | A SELECT or WITH statement the harness sends as the control for sql-guard-statements; it must reach the upstream stub. null builds one (SELECT 1 AS sm_<token>, or a read of the first exact scope.allow entry). |
oauth.issuer | enum | none | 0.3-draft | no | Which OAuth issuer the server under test trusts. stub starts the harness's own authorization server on loopback (flag --oauth): {issuer} and {jwks} name it, and the caller identities ({bearer}, {bearer:a}, {bearer:b}) become tokens it signed for {audience}, sent as Authorization: Bearer. Point the server's issuer, JWKS URL and audience at those placeholders in server.env or server.args. external declares a server that trusts its own issuer: only the checks that need no harness-signed token run (oauth-discovery). null declares nothing. |
oauth.audience | string | none | 0.3-draft | no | The audience (RFC 8707 resource) the stub's tokens carry and the protected-resource metadata must name; the value of {audience}. null uses server.url in canonical form (no fragment, no trailing slash). |
oauth.scopes | string[] | [] | 0.3-draft | no | Scopes the server requires: the control token carries them, and oauth-scope sends a token without them, which must be refused. Empty skips the scope probe. |
oauth.tls | boolean | true | 0.3-draft | no | Serve the stub issuer over https with a throwaway CA made for this run: {issuer} and {jwks} are https:// URLs and {ca_file} is the CA's PEM file. The harness also sets SSL_CERT_FILE, REQUESTS_CA_BUNDLE and NODE_EXTRA_CA_CERTS for the server it starts (to a bundle of that CA and the public roots) unless server.env sets them. false serves plain http, which a hardened server refuses as a trust root (flag --no-oauth-tls). |
oauth.client_id | string | sealmark | 0.3-draft | no | The client_id claim of the tokens the stub mints. |
checks.upstream-slow.expected_sec | number | none | 0.3-draft | no | Seconds the server may take to answer when the upstream is slow before upstream-slow fails (a little slack is added). null uses timeout_probe_sec. |
checks.refusal-quality.patterns | string[] | ["instead","use (the )?[a-z_]+ tool","available","allowed","try","list_[a-z]+","contact","will not (help|succeed)","permanent","not retry"] | 0.3-draft | no | Regular expressions that mark a refusal as routing the caller (naming what is available, what to use instead, that retrying will not help). Replaces the default list. |
checks.instructions-hygiene.deny | string[] | [] | 0.3-draft | no | Regular expressions the server's instructions must not match (a leftover prompt, a product name). Appends across layers. |
output.format | enum | text | 0.1-draft | no | Report format printed to stdout or --out. |
output.formats | string[] | [] | 0.2-draft | no | Several formats at once (written under output.out_dir). Empty means just output.format. |
output.out | string | none | 0.1-draft | no | Write the report to this file instead of stdout (--out). |
output.out_dir | string | none | 0.2-draft | no | Directory for multi-format output. |
output.verbosity | enum | normal | 0.2-draft | no | quiet, normal, or verbose (every evidence line and probe argument). |
output.redact | boolean | true | 0.2-draft | no | Redact secret-looking values in reports. false (--no-redact) is for local debugging only. |
output.template | string | none | 0.2-draft | no | Path of a report template. Templates never change the result data. |
output.baseline | string | none | 0.2-draft | no | Prior JSON report to compare against (--baseline). |
output.write_baseline | string | none | 0.2-draft | no | Write the current JSON report here for later --baseline runs (--write-baseline). |
output.fail_on | string[] | ["any"] | 0.2-draft | no | With a baseline, what decides the exit code: any (the gate and regressions, the default), regressions (regressions only), drift (breaking or notable tool-surface drift). A list: fail_on: [regressions, drift]. |
output.interactive | boolean | none | 0.2-draft | no | deprecated. No longer used and ignored: whether a prompt appears depends on a terminal being attached and on --yes. |
output.exit_policy.fail_under | enum | A | 0.1-draft | no | Exit 1 when a graded pack is below this letter (--fail-under). Default A: anything below an A fails. Use B, C, D or F to relax it, or null (--fail-under none) to turn the gate off. |
output.exit_policy.soft_fail | boolean | false | 0.2-draft | no | A grade below fail_under (or a baseline regression) prints a warning and exits 0. Config errors, nothing graded and server failures still fail (--soft-fail). |
output.exit_policy.regressions_only | boolean | false | 0.2-draft | no | Exit non-zero only for regressions against the baseline. |
output.exit_policy.treat_inconclusive_as | enum | pass | 0.2-draft | no | How --fail-under treats an inconclusive pack. |
output.exit_policy.critical_fail | integer | 0 | 0.2-draft | no | Exit code used when a critical check fails (0 keeps the 0.1 behaviour). |
plugins | list | [] | 0.2-draft | no | Plugin packs: { module, options }. Plugins run in-process with your privileges and always make the run CUSTOM. Appends across layers. |
probes | list | [] | 0.2-draft | no | Probes: valid calls for coverage, and custom checks when an entry has `check`. Entries with `check` make the run CUSTOM. |
notCovered.extra | string[] | [] | 0.2-draft | no | Extra lines for the report's 'not covered' section. Appends across layers. |
active | boolean | false | 0.2-draft | no | Allow the exec pack and active custom probes to run harness-owned programs. Only the --active flag can set it; files and environment cannot. |
yes | boolean | false | 0.2-draft | no | Confirm that --pg may drop and recreate the seed database. Only the --yes flag can set it. |
Profiles
Chosen with the profile key or --profile. A profile may extend another; you can add your own under profiles.
| Profile | Run | Extends | Meaning |
|---|---|---|---|
standard | can be STANDARD | none | The published methodology. A run is STANDARD only when these values are used unchanged. |
quick | always CUSTOM | standard | 2-minute local loop. Drops the slow packs and the cap and timeout probes. Always CUSTOM. |
ci | can be STANDARD | standard | Standard checks with a log summary and JUnit and JSON files, no kept sandbox (the default A gate applies). |
strict | always CUSTOM | standard | Standard with the advisory checks counted in the score. CUSTOM (stricter is still non-standard). |
paranoid | always CUSTOM | strict | Strict plus the exec pack and DNS probes. exec still needs --active on the command line. CUSTOM. |
The tables on this page are generated from the CLI: every key the installed build accepts, with its type, default, the version that added it and whether the standard methodology locks it. This page explains how configuration is read. sealmark config show --origins prints every effective value with the layer it came from.
A config file is optional. Every setting is reachable by a flag, by a SEALMARK_* environment variable or by a file, so a CI job can run with the command line alone. sealmark init writes a starter file. See CLI for the flags.
Layers#
Lowest to highest, each layer overrides the one before:
- built-in defaults;
- the selected profile (
standardunless theprofilekey or--profilesays otherwise; a profile mayextends:another); - your user file:
$XDG_CONFIG_HOME/sealmark/config.yaml(%APPDATA%\sealmark\config.yamlon Windows); sealmark.yaml(or the file named by--config) andsealmark.local.yamlbeside it, which belongs in.gitignore;SEALMARK_*environment variables:SEALMARK_PACKS=core,fs,SEALMARK_SANDBOX__DIR=/tmp/x(a double underscore nests);- command-line flags.
Maps merge key by key and lists replace, except the lists that append (tools.exclude, corpus add and remove, notCovered.extra, plugins). null resets a key to its built-in default. --active and --yes come only from flags: a file or an environment variable cannot switch on the opt-in exec pack or confirm an administrative --pg run.
A key's environment variable is SEALMARK_ plus its path in capitals, with __ for each dot. --start, --header and --bearer have none: they are flags, or server.command, server.args and server.headers in a file.
Two minimal files#
A stdio server that takes a database connection string. The {dsn} placeholder is what lets the db pack start your server once per database role.
name: server-a
version: 1.4.0
server:
command: node
args: ["./server-a.js", "{dsn}"]
An HTTP server that is already running. The harness does not start it.
name: server-d
version: 0.3.0
server:
url: http://localhost:8000/mcp
Server A and Server D are invented examples, like every server and grade on this site.
How the file is checked#
Unknown keys are errors, with a did-you-mean hint and the file and line. All problems found are listed together and stop the run with exit code 2. Among the things that are checked:
- the file exists, is valid YAML and contains a mapping;
serverhascommandorurl; both together are allowed and mean "launch the command and test the URL", but only when both are set in the same place (the same file, the environment, or the flags);- either
serverorservers, never both; server.argsis a list, andserver.envandserver.headersare mappings;packsis a list of pack ids;sql_toolsis a list of{ tool, arg }entries with string values;- a
{dsn:role}placeholder names a role that exists, and every${VAR}reference inserver.url,server.args,server.envandserver.headersis set or has a:-default.
sealmark init starts the file with a yaml-language-server line that points an editor at the JSON Schema shipped in the package (./node_modules/sealmark/schema/sealmark.schema.json), for completion and checking while you type. sealmark config migrate rewrites an older file to the current schema_version.
name and version#
name and version describe the server under test. YAML reads version: 1.10 as the number 1.1, so quote versions that look like numbers: version: "1.10". A three-part version such as 1.4.0 is already text. A grade belongs to one name and version, so change version when the server changes.
server.command and server.args#
The harness runs the command directly, with no shell: write the executable in server.command and each argument in server.args, and use sh -c '...' if you need a shell. Placeholders in the command and in every argument are expanded (see Placeholders). A relative ./ or ../ path in the file is resolved against the folder of the config file, not the folder the server runs in.
server.rootless#
server.rootless (default false) says the server takes no root argument and works in its own working directory. The fs pack then treats that directory, {sandbox}, as the allowed root and runs every path probe, so a server that reads or writes any path fails the escape checks. It only adds coverage, so a STANDARD run stays STANDARD. servers.<name>.rootless sets it per server.
server.env#
server.env values may use placeholders and ${VAR} references.
server:
command: node
args: ["./server-a.js"]
env: { DATABASE_URL: "{dsn}", API_KEY: "${MY_API_KEY}", LOG_LEVEL: info }
A sandboxed server does not inherit your shell's environment: it gets a short allow-list plus what you set here. See Environment.
server.files#
server.files stages small files into the sandbox before the server starts, for a server that reads a config file. It is a list of objects, so it is configuration-file only: no flag and no environment variable can carry it.
server:
command: my-server
args: ["--config", "{sandbox}/config/app.ini"]
files:
- path: "{sandbox}/config/app.ini"
content: |
mode=test
path may begin with {sandbox}, {dir} or {sandbox_home} and must stay inside that directory once expanded (.. is refused with exit code 2). content may use {sandbox}, {sandbox_home}, {listener}, {sqlite_path} and ${VAR}, and never a database placeholder, because the server is started once per database role and a file cannot change between roles: pass the connection string through args or env. A server Sealmark does not start in the sandbox (--url alone, --no-sandbox) cannot have staged files.
server.url and server.headers#
server.url and server.headers accept placeholders and ${VAR} references.
server:
url: http://localhost:8000/mcp
headers: { X-Team: demo, Authorization: "Bearer ${MCP_TOKEN}" }
Secrets: ${VAR} and sealmark.local.yaml#
A secret does not have to be in a file. ${VAR} and ${VAR:-default} in server.command, server.args, server.env, server.url and server.headers are read from your real environment when the run starts, and are never written to a report. A variable that is not set and has no :-default stops the run with exit code 2; write ${VAR:-} to allow an empty value. On the command line, put the reference in single quotes (--bearer '${MCP_TOKEN}') so your shell leaves it for Sealmark to expand.
Values that are specific to your machine, or that you would not commit, go in sealmark.local.yaml next to sealmark.yaml. Everything read from that file is scrubbed from every report format. Never put a real credential or a real database URL in sealmark.yaml. What a report prints and hides of your config is on Environment.
sql_tools#
sql_tools names the tools that take a SQL string, as pairs of the tool name and the name of its SQL argument. The db and sqlite packs send their probes through these tools.
sql_tools:
- { tool: query, arg: sql }
When the list is omitted, the harness finds SQL tools in the server's tools/list answer:
- An argument named
sql,sql_queryorsqlquerycounts on its own. - An argument named
queryorstatementcounts only when the tool's name or description mentions sql, database, postgres or table. - A tool that has other required arguments is kept. The harness fills them: an enum gets its first value, a string a token, a number its minimum or 1, a boolean
false, and a schemadefaultwins. Addargs: { <param>: <value> }to asql_toolsentry (scalar values only) to set them yourself.
Every tool that looks like SQL but was skipped is listed under "Not covered" in the report, with the reason. A list you write adds to or corrects the automatic search and never hides a tool it finds (use tools.exclude to keep one out, which makes the run CUSTOM), and the harness checks your list against the server at run time. A tool the server does not declare, or an argument that is not a string property of that tool, stops the run with exit code 2.
upstream, scope and identities#
For a server that fronts a backend. upstream.kind http starts a harness-owned stub on loopback that the server must use as its backend through {upstream}; upstream.responses and upstream.default_reply script its answers, upstream.credential_header names the header the upstream pack reads and upstream.max_body_kib bounds what it keeps of each request. identities.a and identities.b are secrets: real caller tokens that replace the minted {bearer:a}/{bearer:b} for a server that validates tokens. scope.tools names the identifier parameter of each scoped tool, scope.allow and scope.deny what it may and must never serve (an entry ending in scope.separator is a prefix), and scope.case how names compare. None of these keys is locked, so declaring them keeps a run STANDARD. A worked example is in Verify a server that fronts a backend.
oauth#
For an HTTP server that validates OAuth bearer tokens. oauth.issuer stub (or the --oauth flag) starts a harness-owned authorization server on loopback and sends tokens it signs as Authorization: Bearer; point the server at it through {issuer}, {jwks} and {audience}. external grades only what a server already running tells a client without a token. oauth.audience is the audience the control token carries (default: the server URL). oauth.scopes lists the scopes the server requires: the control token carries them and the oauth-scope check sends a token without them. oauth.client_id is the client_id claim of the tokens. oauth.tls (default true, or --no-oauth-tls) serves the stub over https with a throwaway CA, written to {ca_file}; a server Sealmark starts trusts it through SSL_CERT_FILE, REQUESTS_CA_BUNDLE and NODE_EXTRA_CA_CERTS unless server.env sets them. None of these keys is locked, so declaring them keeps a run STANDARD.
tools#
tools.include and tools.exclude are lists of tool-name patterns that limit which tools Sealmark calls at all (include defaults to ["*"]). tools.exclude appends across layers. tools.overrides fixes a wrong classification, for example legacy_thing: { class: opaque }. A tool the classifier cannot place with enough confidence gets the core checks only and is listed as not covered, so an override is how you say what a tool takes.
checks#
Some check settings are locked by the standard profile. Changing a locked setting is allowed, but the run is then CUSTOM and the report lists the difference. sealmark list-checks shows which checks are locked, and sealmark explain <check-id> lists the settings that affect one.
checks:
fs-size-cap: { enabled: false } # allowed, but the run becomes CUSTOM
robust-input: { max_tools: 10 }
A candidate check (db-privilege, the two core advisories and the checks of the candidate packs scope, sql-guard and upstream) is the exception: its settings are not locked, so a standard run's hash does not contain them, yet switching it off or giving it a severity still makes the run CUSTOM, and --replay never restores them. See Candidate checks.
output#
output.* never changes a grade, so it never makes a run CUSTOM.
output:
formats: [summary, junit, json]
fail_on: [regressions, drift]
Other keys#
The type, default and lock state of every key are in the generated table. The keys that are explained elsewhere:
| Area | Keys | Read |
|---|---|---|
| Report and exit code | output.format, output.formats, output.out, output.out_dir, output.verbosity, output.redact, output.template, output.baseline, output.write_baseline, output.fail_on, output.interactive, output.exit_policy.fail_under, output.exit_policy.soft_fail, output.exit_policy.regressions_only, output.exit_policy.treat_inconclusive_as, output.exit_policy.critical_fail | Output, CLI |
| Sandbox | sandbox.enabled, sandbox.dir, sandbox.keep, sandbox.scrub_env, sandbox.keep_env, sandbox.pack_timeout_sec, sandbox.max_mib | Sandbox |
| Timeouts and load | server.start_timeout_sec, timeouts.connect_ms, timeouts.call_ms, timeouts.probe_ms, timeout_probe_sec, retries.connect, retries.call, concurrency.servers, concurrency.packs | CLI |
| Targets | net.control_url, net.allow_dns, sqlite.engine, db.embedded, db.pg | Methodology |
| Selection | profile, profiles, packs, run.server, notCovered.extra | Profiles |
| Your own checks | probes, plugins | Custom probes, Plugins |
| Command line only | active, yes | CLI |
Placeholders#
Placeholders are replaced just before a server starts. They work in server.command, server.args, the values of server.env, server.url, and the values of server.headers, and {sandbox}, {sandbox_home} and the other harness placeholders also in server.files. They are not replaced anywhere else.
| Placeholder | Expands to |
|---|---|
{dsn} | A postgres:// connection string for the role being tested |
{dsn:analyst}, {dsn:support}, {dsn:admin} | The connection string for that role, whichever role is being tested |
{host}, {port}, {user}, {password}, {database}, {role} | The parts of that connection. The roles and the database exist only for the run and hold synthetic data, so the password is not a secret |
{sandbox} (alias {dir}) | The sandbox root: the directory the harness owns, and the server's working directory |
{sandbox_home} | The sandbox home (HOME, USERPROFILE and APPDATA of the server) |
{sqlite_path} | A seeded SQLite file inside the sandbox |
{upstream} | The URL of the harness's upstream stub, with upstream.kind: http |
{bearer} ({bearer:a}), {bearer:b} | A caller token, minted per run or taken from identities.a/.b; an anonymous session drops every header that holds one |
${VAR}, ${VAR:-default} | An environment variable, read from your real environment when the run starts |
{canary} and {token} exist only in probes:, {matrix.<name>} only in the args of a probe with matrix, and {upstream_token} only in a stub reply body. {listener} exists in probes: and in the content of server.files.
With a database placeholder in the config, the harness starts a Postgres (the embedded one, or your own with --pg and --yes) and the db pack starts the server once per role. With none, or with --no-embedded and no --pg, the db pack is not applicable and core still runs. See roles and server instances.
STANDARD and CUSTOM#
Every report starts with a header that says which it is, and a CUSTOM header lists each difference from the standard. A run is STANDARD only if every setting that affects a grade equals the standard profile, no standard pack was dropped, and no plugin or custom check ran. Adding coverage (the exec pack with --active, a probe without a check:) keeps it standard; weakening anything does not. The profile table above says which profiles can be STANDARD; sealmark config profiles also lists your own. The JSON record carries config.hash, config.standard and config.standardDiff, and --replay reproduces a run from them. A core-only or a CUSTOM run is not eligible for a badge.