| name | authoring-gzcli-challenge-yml |
| description | Use this skill when creating, editing, or reviewing a `challenge.yml` in a gzcli event. Covers the schema (type, value, flags, container, provide, scripts), the value=1000 rule, the flag-template format for DynamicContainer, and the rules for writing the player-facing description (English-only, no hints, no spoilers). |
challenge.yml Authoring
For event-level layout, use authoring-gzcli-ctf-events. The authoritative schema lives in .gzctf/challenge.schema.yaml of any ctf-template directory.
Required fields
name: — challenge title shown to players.
author: — your handle.
description: — markdown / HTML, shown verbatim. See "Description style" below.
type: — exactly one of StaticAttachment, DynamicContainer, StaticContainer.
value: — see "Value" below.
flags: (for static types) or container.flagTemplate: (for DynamicContainer).
provide: "./dist" — set only when dist/ ships artifacts to players.
Value
value: 1000 for every challenge. This matches gzcli's .example/static-attachment/challenge.yml and .example/dynamic-container/challenge.yml, both of which carry value: 1000 # don't touch this value.
- Do not introduce 100 / 150 / 200 / 300 / 350 even when copying from a different template. If the template is at 1000, keep it.
- Scoring is balanced by gzctf's dynamic-scoring curve, not by per-challenge values.
Flag template
- Read the event's flag prefix from
.gzevent or look at neighbouring challenges. Don't hardcode LKS{, flag{, or HTB{ — quote the event's convention.
- For
DynamicContainer: flagTemplate: "<PREFIX>{<slug>_[TEAM_HASH]}". gzctf substitutes [TEAM_HASH] per-team at flag issue time.
- For
StaticAttachment / StaticContainer: a single literal flag string under flags:.
- Do not double-brace the placeholder. The template
<PREFIX>{x_[TEAM_HASH]} produces <PREFIX>{x_<hash>} — exactly one closing brace.
Description style
The description is problem statement, not a hint sheet. A player reading it should learn the lore and the artifact list — not the solving technique.
Allowed:
- Short scenario / lore (1–2 paragraphs).
- List of artifact filenames the player will work with.
- Service endpoint paths, request schema, response shape — the technical contract a player needs to use the service.
- An
nc <host> <port> snippet or POST /endpoint Content-Type: … block.
- A neutral CTA: "Recover the flag." / "Submit the flag." / nothing.
Not allowed:
**Hint:** lines, in any language.
- Leading questions that telegraph the solving path: "What does the attacker actually run on the victim's machine?", "Apa yang terjadi kalau name-nya berisi %p?"
- "The flag lives in X" / "the next-stage payload contains Y" reveals.
- Recommended-tool lists: "Try Ghidra / radare2 / IDA, perhaps photorec / binwalk."
- Bilingual descriptions with
--- splitting English from the local language. Pick one (usually English) and translate the rest.
Self-check: read the description as a player who hasn't solved it. Could a stronger player infer extra solving steps that the lore alone wouldn't suggest? If yes, those sentences are hints — strip them.
Container fields (DynamicContainer / StaticContainer)
containerImage: "{{.slug}}:latest" — gzcli runtime-substitutes {{.slug}} (see "Slug templating" below). Must match the image tag built by scripts.start:.
exposePort: — single port the container listens on.
memoryLimit, cpuCount, storageLimit — keep modest defaults (64–512 MB memory, 1 CPU) unless the workload demands more.
enableTrafficCapture: — true only when the challenge legitimately benefits from gzctf's per-team pcap recording (typically for traffic-analysis style challenges).
Slug templating ({{.slug}})
gzcli runtime-substitutes {{.slug}} and {{.host}} across challenge.yml at sync / watch time (see ProcessChallengeTemplate in internal/gzcli/config/challenges.go). The slug is generated as <event>-<category>-<challenge-name>, lowercased and dash-normalized — e.g. for an event bali, category Web, name Sticky Secrets, the slug is bali-web-sticky-secrets.
Use {{.slug}} literally (with the curly braces) in these fields:
containerImage: "{{.slug}}:latest"
scripts.start: cd src && docker build -t {{.slug}} .
- For compose-based starts:
cd src && docker compose -p {{.slug}} up --build -d
scripts.stop: cd src && docker compose -p {{.slug}} down --volumes (if defined)
scripts.restart: cd src && docker compose -p {{.slug}} restart (if defined)
The upload validator rejects scripts.start / scripts.stop / scripts.restart that don't use the literal {{.slug}} form. Only the exact strings above are accepted (see internal/gzcli/uploadserver/validation.go:384-428). Hardcoding the challenge name (docker build -t sticky-secrets .) will fail upload validation.
flagTemplate: is not processed by gzcli — [TEAM_HASH] is substituted by gzctf at flag-issue time. Don't put {{.slug}} inside flagTemplate:.
For maintainer local-test builds, substitute the slug manually: cd src && docker build -t bali-web-sticky-secrets:latest . (use whatever the resolved slug would be).
Common mistakes
- Description starts with
**Hint:** or contains questions ending in ? that telegraph the solve.
value: is anything other than 1000.
flagTemplate: literal includes two trailing }, or omits the [TEAM_HASH] placeholder.
flags: list set on a DynamicContainer (use container.flagTemplate instead).
provide: "./dist" set without any files under dist/.
containerImage: slug doesn't match the tag scripts.start actually builds.
- Hardcoded challenge name in
containerImage: / scripts.start: instead of {{.slug}}. Upload validator will reject this.
Related sub-skills
- For service implementation in
src/: authoring-gzcli-challenge-src.
- For
dist/ artifacts: authoring-gzcli-challenge-dist.
- For
solver/: authoring-gzcli-challenge-solver.