This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the pwn.college challenge monorepo containing cybersecurity CTF challenges. Challenges are organized as directories under modules (e.g., web-security, etc.) and are built as Docker containers for deployment.
Preferred workflow is to use the repo's Nix flake dev shell:
nix develop
pwnshop test challenges/web-security/path-traversal-1Requirements for nix develop: Linux (x86_64-linux), systemd, sudo, and Nix flakes enabled (experimental-features = nix-command flakes in ~/.config/nix/nix.conf or /etc/nix/nix.conf). See docs/development.md.
All workflows run through the pwnshop CLI (implemented in tools/pwnshop/src/pwnshop/commands and backed by shared helpers in tools/pwnshop/src/pwnshop/lib).
Each subcommand accepts either a direct path or a challenge slug (e.g., challenge/web-security/path-traversal-1). Slugs must contain the base path to all challenges ("challenge"), then the module and challenge.
Primary commands:
# Test a challenge end-to-end
pwnshop test challenges/MODULE_ID/CHALLENGE_ID
# Example: Test path-traversal-1
pwnshop test challenges/web-security/path-traversal-1
# Render a single template file for debugging
pwnshop render challenges/MODULE_ID/CHALLENGE_ID/path/to/file.j2 --output /tmp/rendered-file
# Build a challenge image without tests
pwnshop build challenges/MODULE_ID/CHALLENGE_ID
# List challenges grouped by key, optionally filtered by git history
pwnshop list --modified-since origin/main
# Drop into an interactive shell (use --user/--volume, or append a command)
pwnshop run --user 0 --volume /tmp/debug challenges/web-security/path-traversal-1 /bin/ls -la /challengeDO NOT run these scripts without pwnshop: the dependencies are not installed in the host, and some of these challenges do permanent damage to their environment.
challenges/MODULE_ID/CHALLENGE_ID/challenge/: Challenge source code and artifacts. IF YOU PROVIDE A DOCKERFILE, PUT IT HEREchallenges/MODULE_ID/CHALLENGE_ID/tests_public/: Unencrypted functionality testschallenges/MODULE_ID/CHALLENGE_ID/tests_private/: Encrypted exploitation testschallenges/MODULE_ID/common/: Shared Jinja2 templates for the module
- Files ending in
.j2are Jinja2 templates rendered during build - Templates receive a
challengeobject with seeded RNG functions - Template permissions are preserved when rendering (make .j2 files executable if output should be executable)
- Python templates are auto-formatted with Black
- C templates are auto-formatted with astyle
- Challenge templates should use
{% extends %}not{% include %}when referencing shared templates fromcommon/ - Use
{% block setup %}to set variables on thesettingsnamespace - Call
super()in setup blocks to preserve parent template initialization - The
settingsnamespace is created by flask.py.j2 and passed through the template hierarchy - Random values (endpoints, parameters) are generated by
random_names.j2macro
- Python scripts that need SUID should use shebang:
#!/usr/bin/exec-suid -- /usr/bin/python3 -I - Web services (Flask apps) should set
app.config['SERVER_NAME'] = "challenge.localhost:80"and run on port 80 - Template variables in strings need double braces:
{{variable}}not{variable} - When using randomization in generated code, use the
randomcontext variable (e.g.,{{random.randrange()}}) to evaluate at template time - Binary challenges may not need templates at all - they can be compiled and placed directly in
challenge/
- Every challenge must ship a
Dockerfile.j2(typically{% include "common/Dockerfile.j2" %}) — there is no automatic default. - Copies
challenge/directory to/challengein container - Executes
.setupscript if present during build - Executes
.initscript if present at container startup
- Tests run in temporary containers with a random flag at
/flag - Test files must be named
test_*.pyortest_*.py.j2and must be executable (chmod +x) - Tests receive
FLAGenvironment variable - Public tests verify functionality
- Private tests contain exploitation logic
- Create challenge directory:
challenges/MODULE_ID/CHALLENGE_ID/ - Create or extend common templates in
challenges/MODULE_ID/common/if needed - Add challenge files to
challenge/directory (binaries, scripts, configs, etc.) - If using templates, extend the appropriate common template and set variables in
{% block setup %} - Make executable files and templates executable:
chmod +x challenges/MODULE_ID/CHALLENGE_ID/**/*.j2 - Write
tests_public/test_*.py.j2for functionality verification - Write
tests_private/test_*.py.j2for exploitation verification - Test with:
pwnshop test MODULE_ID/CHALLENGE_ID
{%- extends "common/sqli-pw.py.j2" -%}
{% block setup %}
{{- super() -}}
{%- set settings.pw_name = "pin" -%}
{%- set settings.guest_pw = 1337 -%}
{%- set settings.admin_pw_code = "random.randrange(2**32, 2**63)" -%}
{% endblock %}Can be a C program with or without templating, depending on randomization needs. Will need to be compiled in an executable .setup file or its custom dockerfile
Can be a simple Python/Bash script with or without templating, depending on randomization needs
- Docker is required for building and testing challenges
- The
exec-suidutility is automatically included for SUIDing interpreted programs - Common Dockerfile lives at
challenges/common/Dockerfile.j2; include/extend it or provide a custom one as needed. - Challenge verification should be split between public (functionality) and private (exploitation) tests
- The
challengeobject is available in templates with a seededrandomattribute for deterministic randomization - Use existing common templates where possible (flask.py.j2, cmdi.py.j2, sqli-pw.py.j2, etc.)
- Study existing challenges (cmdi-, path-traversal-) for patterns and conventions