CodePraxis
Developer · CLI

Write your own challenges, in your own repository.

A challenge is a directory, starter code, a test file, instructions. The codepraxis CLI lets you keep those in your own Git repo, edit them in your own editor, check them in seconds, and publish them to your company when they hold up. This guide covers the whole loop.

📦 pip install codepraxis ⚡ Local checks in milliseconds 🔒 Your content stays yours until you publish
At a glance

Why a CLI

Real challenges are codebases, not form fields. They deserve the tools you already use for code, an editor, Git, code review, CI.

🗂️

Content as code

Challenges live in your repo, in branches and pull requests, reviewed like anything else you ship.

⚡

A fast inner loop

codepraxis lint reads the files without running them. validate --local runs the tests on your machine in seconds.

🧪

Proven before it ships

Every pack is checked twice: your reference solution must pass, and the starter must fail.

🐳

The real runner

validate --remote runs the pack in the same container a candidate gets, the check that publishing requires.

🤖

Works with Claude Code

Install a plugin that teaches Claude the pack format and gives it a validate-and-fix loop.

🏢

Scoped to your company

What you publish is owned by the company your API key belongs to, and starts life as a draft.

Try it before you install anything else. codepraxis --example starts a temporary container with a real challenge already loaded and prints a URL. No account, no key, nothing recorded.
Quickstart

From install to published, then inspectable

Everything up to Publish works offline and without an account.

1

Install

Python 3.9 or newer. No other dependencies, the local checks run on a bare interpreter.

# installs the `codepraxis` command
pip install codepraxis
codepraxis --version
2

See what a challenge is

Starts a throwaway container with the featured challenge cloned into it and prints a URL. That page is exactly what a candidate sees. Add --open to launch your browser.

codepraxis --example

Build a SQL Agent
  Medium · Python, SQL · 60 minutes

  https://…azurecontainerapps.io/?folder=/home/praxis/querybot

The container expires on its own, and nothing you do in it counts as an attempt.

3

Scaffold your own

Generates a complete, already working pack, solution passes, starter fails. That gives you a correct starting point and proves your setup works before you have written anything.

codepraxis new my_challenge
codepraxis validate --local my_challenge

my_challenge  (local)
  solution  2/2 passed
  starter   0/2 passed
  PASSED  0.3s
4

Make it yours

Four things to edit. Re-run lint and validate --local as often as you like, both are fast enough to sit in a watch loop.

  • ._course_data/feature.md , the brief the candidate reads.
  • source/ , what they start from. Stubs only; never a working implementation.
  • ._tests/test_1.py , how it is graded.
  • ../solution/ , your reference answer, beside the pack so it is never shipped.
5

Sign in

Only needed for publishing and catalog commands. Create a key in the dashboard under Settings → API keys with the challenges:write scope. Add challenges:read if you want read-only listing keys.

codepraxis --login

Logged in as you@company.com
Publishing as: Acme Corp

The key is stored at ~/.config/codepraxis/config.json with owner-only permissions. In CI, set CODEPRAXIS_TOKEN instead and skip the prompt.

6

Publish

Runs a full validation in the real runner first, shows you which company it will belong to, and asks before creating anything. When the platform can start a preview container, the command prints that URL too.

codepraxis --publish my_challenge

Validating my_challenge in the runner before publishing…
  solution  2/2 passed · starter 0/2 passed · PASSED

About to publish my_challenge to Acme Corp, as a draft.
Continue? [y/N] y

my_challenge draft for Acme Corp
  challenge 123, version 456
  https://…azurecontainerapps.io/?folder=/home/praxis/my_challenge

It lands as a draft. Making it available to candidates is a separate, deliberate step in the dashboard, or --live if you are sure.

7

List or edit company questions

Once questions exist in your company catalog, you can list them or update metadata from the CLI. --edit also starts a preview container so you can inspect the question.

codepraxis --list
codepraxis --edit 123 --title "Debug webhook delivery" --status draft
codepraxis --edit 123 --open
Pack anatomy

What a challenge is made of

A directory the platform mounts into a container. The candidate gets source/; the grader runs ._tests/test_1.py.

my_challenge/
├── metadata.json            # {"name": "my_challenge"}, becomes the workspace folder
├── backend.conf             # {"BACKEND": "AI", "LANGUAGE": "PYTHON"}
├── setup.sh                 # optional; installs dependencies
├── source/                  # what the candidate starts from
│   ├── main.py
│   └── README.md            # current state + how to run it
├── ._tests/
│   └── test_1.py            # a `testCases` class
└── ._course_data/
    ├── course_toc.json      # selects the active test file
    └── feature.md           # the Instructions tab
solution/                    # SIBLING of the pack, never uploaded
The solution sits beside the pack, not inside it. Everything inside the pack directory is uploaded and mounted into a candidate's container. A solution/ folder nested in the pack would hand every candidate the answer, codepraxis lint treats that as an error.
The grader
class testCases:
    def __init__(self, user_wxpace) -> None:
        self.RUN = 2                    # cases shown to the candidate
        self.RunCaseInputs = ["1 2 3", "-5 5"]
        self.userWxpace = user_wxpace   # the workspace path
        self.default_timeout_window = 20000   # MILLISECONDS
        self.msg = ""

    def test_case_1(self, timeout_window=20000, override=1):
        ...
        self.msg = "PASS"              # this decides pass/fail
        return expected_text, actual_output

Five rules the runner enforces. codepraxis lint checks all of them without executing anything.

Dependencies go in setup.sh, not source/requirements.txt. The runner imports your test file immediately after starting setup.sh, so a package that setup.sh installs may not be there yet at import time, import it inside the test method instead of at module level.
Checking your work

Two kinds of check, three verdicts

One reads your files. The other runs them. You will use both, at different moments.

CommandWhat it doesCostAuthoritative?
codepraxis lint Reads the pack and reports problems. Never imports or runs pack code. Milliseconds No, structure only
codepraxis validate --local Runs your tests against both fixtures on your machine. Seconds No, advisory
codepraxis validate --remote Runs the pack in the production container. ~1 minute Yes, required to publish

validate runs the static checks first and stops if they find an error, executing a pack whose grader cannot even be constructed only produces a confusing traceback.

✓

The rule that matters most

Every pack is run twice:

  • solution -source/ with your reference answer applied. Must pass everything.
  • starter -source/ exactly as a candidate receives it. Must fail.

The starter run is the one authors forget. If the starter already passes, the tests do not discriminate, every candidate scores full marks without writing a line. Ask of each test: what in source/ makes this fail today? If nothing does, the test is decorative.

Verdicts
VerdictMeaningExit code
PASSEDSolution passes, starter fails. The pack is sound.0
FAILEDSomething is wrong with the pack.1
INCONCLUSIVEThis machine lacked what the pack needs, usually a model endpoint. Says nothing about the pack.0
OKlint only: no problems found.0
Packs that call a model. By default there is no model endpoint on your machine, so those cases come back unverifiable rather than failed. Set OPENAI_API_KEY (and OPENAI_BASE_URL if you use a compatible provider) and they run for real, at which point a failure is a real failure.

Local checks are honest about their limits: they do not run setup.sh, do not have the container image's packages, and run on your OS rather than linux/amd64. Anything they cannot verify is reported as a note. Publishing always requires --remote.

Publishing

What happens when you publish

Publishing is deliberately strict, because a published challenge can be put in front of a candidate immediately.

In CI
# No prompts: token from the environment, --yes to skip confirmation
export CODEPRAXIS_TOKEN="sk_live_…"

codepraxis lint
codepraxis --publish my_challenge --yes
Point at a different environment with CODEPRAXIS_API_URL. It defaults to the production platform, so you only need it for staging or a local server.
Company catalog

List and edit existing questions

The CLI can also work with questions that already exist for the company attached to your API key. Listing is read-only; editing updates metadata and opens a container preview.

List questions
codepraxis --list

123    Debug webhook delivery    draft      v456
124    Build a SQL Agent         published  v457
Edit metadata and inspect
codepraxis --edit 123 --title "Debug webhook delivery" --difficulty 2

Updated question 123: Debug webhook delivery
  https://…azurecontainerapps.io/?folder=/home/praxis/debug_webhook_delivery

Metadata edits include --title, --description, --status, --difficulty, --max-time, --max-attempt, --tech-stack, and --ai-enabled / --no-ai-enabled. Use --open to launch the returned container URL in your browser.

Code changes still go through packs. Use the preview container to inspect what candidates see, but make source/test changes locally, validate them, and publish a new version.
Publishing an edit

Changing the starter code, the tests or the brief means re-publishing the pack. Pass the question’s id, or you get a second copy of it.

codepraxis --publish my_challenge --challenge-id 123

my_challenge updated for Acme Corp (draft)
  challenge (new version of) 123, version 458

With --challenge-id the platform validates as usual and then adds a new version to question 123, keeping its id, its assignments and its history. Without it, publish always creates a new question, correct the first time, a duplicate every time after. Versions are additive, so candidates already mid-assessment keep the version they started on.

Deleting a question
codepraxis --delete 123

Permanently delete question 123? This cannot be undone.
Continue? [y/N]

Deletion is refused once the question has been assigned to anyone, attempts and reports still point at it, and removing it would orphan work candidates and reviewers depend on. To take an assigned question out of circulation, put it back in draft instead:

codepraxis --edit 123 --status draft
Optional

Authoring with Claude Code

If you use Claude Code, one command teaches it the pack format so you are not explaining the contract every time.

codepraxis --install claude-plugin

It writes a local plugin into your project and prints the two commands to enable it. You get:

Reference

Commands

CommandWhat it doesNeeds a key?
codepraxis --exampleOpen the featured challenge in a live container. --open launches your browser.No
codepraxis new NAMEScaffold a pack that already validates. --backend, --language, --force.No
codepraxis lint [PACK]Static checks, no execution. Defaults to every pack found.No
codepraxis validate [PACK]--local (default) or --remote. --fixture to run just one.Only --remote
codepraxis --loginStore an API key and show which company it publishes as.-
codepraxis --publish PACKValidate remotely, then publish. --challenge-id to publish an edit, --yes, --live, --validation-run-id.Yes
codepraxis --listEvery question your company owns, drafts included.Yes
codepraxis --edit IDUpdate metadata. --open starts a preview container.Yes
codepraxis --delete IDDelete a question. Refused once it has been assigned.Yes
codepraxis --listList questions owned by your API key's company. Add --json for machine-readable output.Yes
codepraxis --edit IDUpdate question metadata and return a preview container URL. Add --open to launch it.Yes
codepraxis --install claude-pluginInstall the Claude Code plugin into this project.No

--root points any pack command at a different directory. lint, validate, --publish and --list also take --json for machine-readable output, always one document, however many packs ran, and -v to show passing cases and the checks a tier could not verify.

Environment

VariablePurpose
CODEPRAXIS_TOKENAPI key. Skips the login prompt, the CI path.
CODEPRAXIS_API_URLPlatform base URL. Defaults to production.
OPENAI_API_KEYLets model-dependent cases run in --local instead of reporting unverifiable.
OPENAI_BASE_URLAn OpenAI-compatible endpoint, if not the default.

Getting unstuck

What you seeWhat it means
“the starter passes every test”source/ contains a working implementation, or the assertions are too weak to notice it is missing.
“__init__ takes 2 arguments after self”Your testCases.__init__ has an extra parameter. The runner passes only the workspace path.
“is zero-padded, so it sorts ambiguously”Rename test_case_01 to test_case_1.
“is inside the pack, so it would be uploaded”Move solution/ out of the pack directory, beside it.
A second copy appeared after an editPublishing without --challenge-id always creates a new question. Delete the duplicate and re-publish with the id.
“Not authenticated”Run codepraxis --login, or set CODEPRAXIS_TOKEN.
“lacks the challenges:write scope”Reissue the key with that scope in the dashboard.
“lacks the challenges:read scope”Use a key with challenges:read, challenges:write, or * for --list.
“returned no container URL”The question exists, but the preview container did not start. Retry --edit ID --open or inspect it in the dashboard.
Questions about authoring? Talk to us →