This schema defines the structure for describing CTF challenges. It ensures a unified format for challenge descriptions and includes all necessary information for automation.
Both YAML and JSON versions of this schema are used. This README documents every supported field and enumerates all allowed values so maintainers and challenge authors have a single reference.
Examples of the schema in use can be found in the examples directory.
The schema is an object. At minimum a challenge SHOULD include the following fields:
enabled(boolean) - whether the challenge is enabled. Default:true.name(string) - human-readable name of the challenge.slug(string) - machine-friendly slug. Must match the regex^[a-z0-9-]+$.author(string) - author or owner of the challenge.category(string) - see allowed values.difficulty(string) - see allowed values.tags(array[string], optional) - freeform tags.type(string) -static|shared|instanced.instanced_type(string, optional) -web|tcp|none(useful forinstancedchallenges).flag(string | array | array of objects) - one or more flags (see Flags section).points(integer, optional) - starting point value (1..10000). Default 1000.min_points(integer, optional) - minimum point value (1..1000). Default 100.
See all value types in Field reference.
These are the explicit allowed values used by the schema and tooling.
-
category(one of):webforensicsrevcryptopwnboot2rootosintmiscblockchainmobiletest
-
difficulty(one of):beginnereasyeasy-mediummediummedium-hardhardvery-hardinsane
-
type(one of):static- challenge is a static artifact (files, puzzles, etc.).shared- external network service; may require aconnectionstring. Instance is shared among all teams.instanced- an instance-per-team/service is launched (seeinstanced_type).
-
instanced_type(one of):web- an HTTP/web instance.tcp- a plain TCP/port-based instance.none- not instanced / no special instance behavior.
-
tags: each tag is a string and should match the regex:^[a-zA-Z0-9-_:;? ]+$(allows letters, numbers, hyphen, underscore, colon, semicolon, question mark and spaces). -
slug: must match^[a-z0-9-]+$, min length 1, max length typically 50 (see schema file for authoritative limits). -
points: integer between 1 and 10000 (inclusive). Default 1000. -
min_points: integer between 1 and 1000 (inclusive). Default 100.
version(string, optional) - schema version (e.g.1.0.0or1). Fully optional; if omitted the latest schema version is assumed.enabled(boolean) - whether the challenge is active.name(string) - e.g. "Demo Challenge".slug(string) - e.g.demo-challenge.author(string) - e.g.The Mikkel.category(string) - see enumerations above.difficulty(string) - see enumerations above.tags(array[string], optional) - e.g.["demo", "example"].type(string) -static,shared, orinstanced.instanced_type(string, optional) -web,tcp, ornone.instanced_name(string, optional) - slug of the instanced challenge. If omitted the challengeslugis typically used.instanced_subdomains(array[string], optional) - list of subdomains used for instanced web challenges.
Each entry may optionally be prefixed withweb:ortcp:(e.g.,web:api,tcp:service, or justadmin). The prefix indicates the protocol associated with the subdomain. All entries must match the pattern^((web|tcp):)?[a-z0-9-]+$.connection(string, optional) - a connection URI or host:port string forsharedchallenges (max length 255).flag(string | array | array of objects) - see Flags section.points(integer, optional) - initial points awarded for solving.min_points(integer, optional) - floor for dynamic scoring.decay(integer, optional) - decay for dynamic scoring. Default:75.description_location(string, optional) - path to a file with the challenge description (e.g.description.md).prerequisites(array[string], optional) - other challenge slugs that must be solved first.dockerfile_locations(array[object], optional) - list of build instructions; each object has:context(string) - build context directorylocation(string) - path to the Dockerfileidentifier(string | null, optional) - optional label identifying the image
handout_dir(string, optional) - directory with handout files.
flag supports several shapes to accommodate static and advanced workflows.
-
Single string flag:
YAML
flag: flag{flag}
JSON
"flag": "flag{flag}"
-
Array of string flags:
YAML
flag: - flag{flag1} - flag{flag2}
JSON
"flag": [ "flag{flag1}", "flag{flag2}" ]
-
Array of objects (supporting case sensitivity and metadata):
YAML
flag: - flag: flag{flag1} case_sensitive: true - flag: flag{flag2} case_sensitive: false
JSON
"flag": [ { "flag": "flag{flag1}", "case_sensitive": true }, { "flag": "flag{flag2}", "case_sensitive": false } ]
Each flag element may be dynamic, null, or a normal flag string. If case_sensitive is omitted, flag checking defaults to case-sensitive behavior.
The default flag format is flag{...}. However, the schema allows for 2-10 chars as the flag delimiter. For example, FLAG[...], CTF{...}, SECRET(...), etc.
The delimiter must be a word char (a-z, A-Z, 0-9, _).
If you want to use a set flag format for your CTF, you can fork the schema and modify the regex pattern for the flag field in the schema.json file. Look for the flag property and adjust the pattern attribute to match your desired format.
The flag can be a maximum of 1000 characters in length, including the delimiter.
dockerfile_locations is an array of objects. Example:
YAML
dockerfile_locations:
- context: demo/
location: demo/Dockerfile
identifier: webJSON
"dockerfile_locations": [
{
"context": "demo/",
"location": "demo/Dockerfile",
"identifier": "web"
}
]This tells automation: from demo/ context, use demo/Dockerfile and tag the resulting image using identifier when provided.
The identifier field accepts either a string or null (i.e. "type": ["string", "null"] in the schema). Use null, an empty string, or the literal value None to indicate "no suffix" for the image tag. Example where no identifier/suffix is used:
YAML
dockerfile_locations:
- context: demo/
location: demo/Dockerfile
identifier: nullJSON
"dockerfile_locations": [
{
"context": "demo/",
"location": "demo/Dockerfile",
"identifier": null
}
]YAML
enabled: true
name: "Demo Challenge"
slug: "demo-challenge"
author: "The Mikkel"
category: "misc"
difficulty: "easy"
type: "static"
instanced_type: "none"
flag: "flag{d3m0_fl4g}"
points: 1000
min_points: 100
description_location: "description.md"JSON
{
"$schema": "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json",
"enabled": true,
"name": "Demo Challenge",
"slug": "demo-challenge",
"author": "The Mikkel",
"category": "misc",
"difficulty": "easy",
"type": "static",
"instanced_type": "none",
"flag": "flag{d3m0_fl4g}",
"points": 1000,
"min_points": 100,
"description_location": "description.md"
}YAML
enabled: true
name: "Example Challenge"
slug: "example-challenge"
author: John Smith
category: web
difficulty: easy
tags:
- web
- easy
type: static
instanced_type: none
instanced_name: example-challenge
instanced_subdomains:
- example
- test
flag:
- flag: flag{flag1}
case_sensitive: true
- flag: flag{flag2}
case_sensitive: false
points: 1000
min_points: 100
description_location: description.md
prerequisites:
- prerequisite-challenge
dockerfile_locations:
- location: src/web/Dockerfile
context: src/web/
identifier: web
- location: src/bot/Dockerfile
context: src/bot/
identifier: bot
handout_dir: handout
connection: http://example.comJSON
{
"$schema": "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json",
"enabled": true,
"name": "Example Challenge",
"slug": "example-challenge",
"author": "John Smith",
"category": "web",
"difficulty": "easy",
"tags": [
"web",
"easy"
],
"type": "static",
"instanced_type": "none",
"instanced_name": "example-challenge",
"instanced_subdomains": [
"example",
"test"
],
"flag": [
{
"flag": "flag{flag1}",
"case_sensitive": true
},
{
"flag": "flag{flag2}",
"case_sensitive": false
}
],
"points": 1000,
"min_points": 100,
"description_location": "description.md",
"prerequisites": [
"prerequisite-challenge"
],
"dockerfile_locations": [
{
"location": "src/web/Dockerfile",
"context": "src/web/",
"identifier": "web"
},
{
"location": "src/bot/Dockerfile",
"context": "src/bot/",
"identifier": "bot"
}
],
"handout_dir": "handout",
"connection": "http://example.com"
}Add the following top-line to your YAML files to enable editor validation and autocompletion:
# yaml-language-server: $schema=https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.jsonAdd the following to your JSON files to enable editor validation and autocompletion:
{
"$schema": "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json"
}You may replace the URL with a local path to schema.json or to a specific version/release in the GitHub repository, such as with: https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/tags/vX.X.X/schema.json.
- Prefer
slug-style names (lowercase, hyphen-separated) for identifiers andinstanced_subdomains. - Keep
pointsandmin_pointswithin the defined ranges to avoid validation failures in automation. - Use
dockerfile_locationswhen challenge runtime requires custom images; include anidentifierwhen multiple images exist. - When in doubt about a field's format, open the canonical
schema.jsonfor the challenge folder.
We welcome contributions of all kinds, from code and documentation to bug reports and feedback!
Please check the Contribution Guidelines (CONTRIBUTING.md) for detailed guidelines on how to contribute.
To maintain the ability to distribute contributions across all our licensing models, all code contributions require signing a Contributor License Agreement (CLA).
You can review the CLA here. CLA signing happens automatically when you create your first pull request.
To administrate the CLA signing process, we are using CLA assistant lite.
A copy of the CLA document is also included in this repository as CLA.md.
Signatures are stored in the cla repository.
This schema and repository is licensed under the EUPL-1.2 License.
You can find the full license in the LICENSE file.
We encourage all modifications and contributions to be shared back with the community, for example through pull requests to this repository.
We also encourage all derivative works to be publicly available under the EUPL-1.2 License.
At all times must the license terms be followed.
For information regarding how to contribute, see the contributing section above.
CTF Pilot is owned and maintained by The0Mikkel.
Required Notice: Copyright Mikkel Albrechtsen (https://themikkel.dk)
We expect all contributors to adhere to our Code of Conduct to ensure a welcoming and inclusive environment for all.