ctfd-warboard is a practical teamwork tool for CTF students using a CTFd-based competition site.
It takes challenges from CTFd, saves them into a shared private git repository, and lets your team coordinate through either Discord slash commands or a terminal CLI. The goal is simple: everyone can see who is working on what, write notes, save files, submit solves, and keep the team workspace synchronized.
Useful guides:
During a CTF, teams often lose time because work is scattered:
- One person has challenge files on their laptop.
- Another person writes notes in Discord.
- Someone solves a challenge but forgets to tell the team.
- Multiple teammates attack the same challenge without knowing it.
- The CTFd site gets spammed by many refreshes and downloads.
ctfd-warboard gives the team one shared working board.
It fetches challenges carefully from CTFd, stores the data locally, writes team activity to git, and keeps Discord and CLI users looking at the same state.
There are five moving parts:
| Part | What it is | Why students care |
|---|---|---|
| CTFd | The competition website | Source of challenges, files, points, and flag submission |
| ctfd-warboard core | Shared TypeScript logic | Handles CTFd, cache, validation, files, and git sync |
| Discord bot | Team interface in Discord | Best for live coordination during a CTF |
| CLI | Terminal interface named ctfd |
Best for local work, scripts, writeups, and offline reading |
| Private git repo | Shared workspace | The source of truth for notes, claims, solves, files, and writeups |
You can use only the Discord bot, only the CLI, or both. The git repo connects everything.
flowchart LR
CTFd["CTFd competition site<br/>challenges, files, flags"]
Core["ctfd-warboard core<br/>rate limit, cache, validation"]
Git["Private git repo<br/>shared team state"]
Bot["Discord bot<br/>slash commands, threads, polling"]
CLI["CLI: ctfd<br/>terminal commands"]
Students["Students<br/>claim, note, solve, writeup"]
CTFd -->|"1 request/sec fetch<br/>refresh only what changed"| Core
Core -->|"writes challenge folders<br/>claims, notes, solves"| Git
Git -->|"pull/push sync"| Bot
Git -->|"pull/push sync"| CLI
Bot -->|"Discord workflow"| Students
CLI -->|"terminal workflow"| Students
Students -->|"flags"| Bot
Students -->|"flags"| CLI
Bot -->|"submit solve"| CTFd
CLI -->|"submit solve"| CTFd
Think of one CTF as one project folder.
Inside that folder, each challenge has its own directory:
web/
sql-injection/
challenge.md # description copied from CTFd
notes.md # team notes
solve.md # final explanation / writeup
files/ # downloaded challenge attachments
When Alice claims a challenge, ctfd-warboard records that in git. When Bob adds a note in Discord, that note is saved in the same repo. When someone writes a solve explanation, it also lives in the repo.
That means the team does not need to search old Discord messages to reconstruct the solution later.
| Question | Use Discord bot | Use CLI |
|---|---|---|
| I want the easiest team workflow | Yes | Optional |
| I work mostly in terminal | Optional | Yes |
| I want automatic polling for new challenges | Yes | No, manual ctfd refresh |
| I want to edit writeups in my editor | Optional | Yes |
| I do not have git access | Yes, if bot is already set up | No |
| I am the team admin | Usually yes | Useful too |
Most student teams should start with the Discord bot, then let power users join the same repo with the CLI.
Use this path if you want to work from your laptop terminal.
Install:
- Bun v1.0 or newer
- Git
- Access to the team's private git repo
Then clone and install:
git clone https://github.com/gl0bal01/ctfd-warboard
cd ctfd-warboard
bun install
cd packages/cli && bun link && cd ../..Check that the command exists:
ctfd --helpIf you do not want to link the CLI globally, run it from source:
bun run cli -- statusYour handle is the name shown in claims and notes.
Create ~/.ctfd-warboard/config.json:
{
"handle": "alice"
}Replace alice with your CTF handle.
Use this if you are creating the team workspace.
First create an empty private repo on GitHub, GitLab, Gitea, or another git host. The repo must already exist and you must have write access.
Then initialize:
ctfd init https://ctf.example.com --repo git@github.com:team/example-ctf.gitctfd-warboard will ask for credentials if needed.
You can also provide credentials directly:
ctfd init https://ctf.example.com \
--repo git@github.com:team/example-ctf.git \
--username myuser \
--password mypassOr use a CTFd API token:
ctfd init https://ctf.example.com \
--repo git@github.com:team/example-ctf.git \
--token ctfd_abc123Environment variables work too:
export CTFD_TOKEN=ctfd_abc123
ctfd init https://ctf.example.com --repo git@github.com:team/example-ctf.gitUse this if a teammate or the Discord bot already created the repo:
ctfd join git@github.com:team/example-ctf.gitNow you have the same challenge files, notes, claims, and writeups as the rest of the team.
ctfd statusShows all challenges, categories, points, solved state, and assignees.
ctfd claim web/sql-injectionMarks the challenge as yours so teammates know you are working on it.
ctfd note web/sql-injection "Login blocks single quotes. Trying hex encoding next."Adds a timestamped note to the challenge.
ctfd writeup web/sql-injectionOpens solve.md in your $EDITOR.
ctfd solve web/sql-injectionPrompts for the flag, marks the challenge solved, and submits to CTFd.
ctfd unclaim web/sql-injectionReleases the challenge if you stop working on it.
ctfd refreshFetches the latest challenge state from CTFd.
More management commands:
ctfd list
ctfd use example-ctf
ctfd scoreboard
ctfd archive example-ctfUse this path if your team wants to coordinate inside Discord.
- Go to the Discord Developer Portal.
- Click New Application.
- Open the Bot tab and create or reset the bot token.
- Copy the bot token. This becomes
DISCORD_TOKEN. - Open General Information and copy the Application ID. This becomes
DISCORD_CLIENT_ID. - In the Bot tab, enable these privileged gateway intents:
- Presence Intent
- Server Members Intent
- Message Content Intent
The bot needs permission to create channels, create threads, send messages, attach files, and manage challenge discussions.
Use this invite URL and replace YOUR_CLIENT_ID:
https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&scope=bot+applications.commands&permissions=395405552656
git clone https://github.com/gl0bal01/ctfd-warboard
cd ctfd-warboard
bun install
cp .env.example .envEdit .env:
DISCORD_TOKEN=your_bot_token
DISCORD_CLIENT_ID=your_application_id
CTFD_TOKEN=your_ctfd_token
POLL_INTERVAL_MINUTES=10If the CTFd instance does not support API tokens, use username and password instead:
CTFD_USERNAME=your_username
CTFD_PASSWORD=your_passwordbun run bot:deployDiscord global commands can take 1 to 5 minutes to appear.
bun run botWhen the bot is online, start a CTF from Discord:
/ctf start url:https://ctf.example.com repo:git@github.com:team/example-ctf.git
The bot will:
- Fetch challenges from CTFd.
- Download challenge files.
- Create Discord structure.
- Save challenge folders locally.
- Commit and push the result to the private git repo.
- Poll for updates every 10 minutes by default.
Admin commands:
/ctf start <url> <repo>
/ctf join <repo>
/ctf list
/ctf refresh
/ctf archive
/ctf status
Challenge thread commands:
/claim
/unclaim
/note <message>
/writeup <content>
/solve <flag>
Files uploaded into a challenge thread are saved into that challenge's folder in git.
Here is a normal team flow:
- Team admin creates an empty private git repo.
- Admin starts ctfd-warboard with either CLI or Discord bot.
- ctfd-warboard fetches challenge data from CTFd at a safe speed.
- The bot creates Discord channels and threads, or CLI users work from terminal.
- A student claims a challenge before working on it.
- Students add notes as they test ideas.
- Challenge files, notes, writeups, and claim state are committed to git.
- When a flag is found, a student runs
/solveorctfd solve. - The result is submitted to CTFd and saved for the team.
- After the CTF, the repo already contains the material needed for writeups and review.
By default, ctfd-warboard stores state under ~/.ctfd-warboard/.
~/.ctfd-warboard/
config.json
repos/
example-ctf/
.ctfd.json
claims/
web/
sql-injection/
challenge.md
notes.md
solve.md
files/
ctfs/
example-ctf/
cache.json
meta.json
locks/
example-ctf.lock
Important files:
| File | Purpose |
|---|---|
config.json |
Your handle, editor, active CTF, and default settings |
.ctfd.json |
CTF metadata committed to git, without secrets |
challenge.md |
Challenge description from CTFd |
notes.md |
Team notes, appended with timestamps |
solve.md |
Final writeup |
cache.json |
Local cache of CTFd data |
meta.json |
Local CTF metadata |
Notes are append-only to reduce git conflicts:
## [2026-03-21T14:30:00Z] @alice
Tried basic SQLi on login form. Single quotes are blocked.
---Do not commit tokens, passwords, session cookies, or private keys.
Use .env for local bot development. It is git-ignored.
Use ~/.ctfd-warboard/bot.json or environment variables for CTFd credentials:
{
"ctfs": {
"example-ctf": {
"token": "ctfd_abc123"
}
}
}Then restrict the file:
chmod 600 ~/.ctfd-warboard/bot.jsonFor production, store bot secrets in /etc/ctfd-warboard/bot.env with 0600 permissions. See docs/deployment.md.
ctfd-warboard also includes validation for risky inputs such as private-network CTFd URLs, path traversal in filenames, unsafe repo paths, and unsafe commit messages.
This repository is a Bun + TypeScript monorepo.
packages/
core/ # CTFd client, cache, git sync, config, validation, shared types
cli/ # terminal command wrapper around core
bot/ # Discord slash commands, polling, thread/file handling
The important design rule is:
core = real behavior
cli = terminal interface
bot = Discord interface
So if you fix how a challenge is fetched, cached, validated, or written to git, that code probably belongs in packages/core.
Install dependencies:
bun installRun the CLI from source:
bun run cli -- statusRun the bot locally:
bun run botDeploy slash commands:
bun run bot:deployCheck code quality:
bun run lint
bun run typecheck
bun test --recursiveRun the full project check:
make checkFor real CTF use, run the Discord bot on a VPS or always-on server. A laptop that sleeps will miss polling and Discord interactions.
Short version:
git clone https://github.com/gl0bal01/ctfd-warboard /opt/ctfd-warboard
cd /opt/ctfd-warboard
make installThen create /etc/ctfd-warboard/bot.env, install the systemd service, and start it:
make service-install
make service-startRead the full production guide before using this in a serious event:
The bot cannot push to git.
Create the remote repo before running /ctf start, then make sure the bot server's SSH key has write access. The bot only accepts SSH remotes (git@host:owner/repo.git); HTTPS remotes are rejected because the bot runs non-interactively and cannot supply credentials. (The CLI still allows HTTPS for interactive use.)
Slash commands are missing or stuck on "Loading...".
Wait 1 to 5 minutes after bun run bot:deploy. Re-run the deploy command if they never appear.
The CLI cannot see the CTF created by the bot.
The CLI and bot must use the same CTFD_WARBOARD_HOME, or the CLI user must run ctfd join <repo>.
CTFd token authentication fails.
Some CTFd instances do not support API tokens. Use CTFD_USERNAME and CTFD_PASSWORD.
A challenge was added but /solve fails.
Run /ctf refresh in Discord or ctfd refresh in the terminal so the cache has the latest challenge metadata.
Two people edit the same writeup.
Use git pull, resolve the conflict, and keep both useful parts. Notes are append-only, so they usually conflict less than writeups.
- Bun is required. Node.js is not a drop-in replacement.
- The bot must stay online for polling and Discord slash commands.
- The private git remote must already exist and be writable. For the bot it must be an SSH remote (
git@...); HTTPS remotes are rejected. - Very large CTFs can make initial setup slow because files must be downloaded and committed.
- One shared data root per bot host is expected. Do not run multiple bot instances against the same root.