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.
Real challenges are codebases, not form fields. They deserve the tools you already use for code, an editor, Git, code review, CI.
Challenges live in your repo, in branches and pull requests, reviewed like anything else you ship.
codepraxis lint reads the files without running them. validate --local runs the tests on your machine in seconds.
Every pack is checked twice: your reference solution must pass, and the starter must fail.
validate --remote runs the pack in the same container a candidate gets, the check that publishing requires.
Install a plugin that teaches Claude the pack format and gives it a validate-and-fix loop.
What you publish is owned by the company your API key belongs to, and starts life as a draft.
codepraxis --example starts a temporary container with a real challenge already loaded and prints a URL. No account, no key, nothing recorded.Everything up to Publish works offline and without an account.
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
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.
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
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.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.
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.
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
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
solution/ folder nested in the pack would hand every candidate the answer, codepraxis lint treats that as an error.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.
testCases(workspace_path). A second parameter breaks the pack.self.msg == "PASS" decides the verdict. The return value is what the candidate panel displays, not whether the case passed.test_case_01 and test_case_1 collide.RUN is the visible count. Extra test_case_* methods beyond it still run as hidden cases. RunCaseInputs must have RUN entries.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.One reads your files. The other runs them. You will use both, at different moments.
| Command | What it does | Cost | Authoritative? |
|---|---|---|---|
| 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.
Every pack is run twice:
source/ with your reference answer applied. Must pass everything.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.
| Verdict | Meaning | Exit code |
|---|---|---|
| PASSED | Solution passes, starter fails. The pack is sound. | 0 |
| FAILED | Something is wrong with the pack. | 1 |
| INCONCLUSIVE | This machine lacked what the pack needs, usually a model endpoint. Says nothing about the pack. | 0 |
| OK | lint only: no problems found. | 0 |
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 is deliberately strict, because a published challenge can be put in front of a candidate immediately.
--validation-run-id if you have one.--live.# No prompts: token from the environment, --yes to skip confirmation
export CODEPRAXIS_TOKEN="sk_live_…"
codepraxis lint
codepraxis --publish my_challenge --yes
CODEPRAXIS_API_URL. It defaults to the production platform, so you only need it for staging or a local server.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.
codepraxis --list
123 Debug webhook delivery draft v456
124 Build a SQL Agent published v457
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.
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.
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
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:
/codepraxis:new , design the question architecture, implement the pack, validate locally, publish through the remote runner, and return the preview URL./codepraxis:validate , validate and fix what fails, in a loop.testCases contract and the two-fixture rule.| Command | What it does | Needs a key? |
|---|---|---|
| codepraxis --example | Open the featured challenge in a live container. --open launches your browser. | No |
| codepraxis new NAME | Scaffold 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 --login | Store an API key and show which company it publishes as. | - |
| codepraxis --publish PACK | Validate remotely, then publish. --challenge-id to publish an edit, --yes, --live, --validation-run-id. | Yes |
| codepraxis --list | Every question your company owns, drafts included. | Yes |
| codepraxis --edit ID | Update metadata. --open starts a preview container. | Yes |
| codepraxis --delete ID | Delete a question. Refused once it has been assigned. | Yes |
| codepraxis --list | List questions owned by your API key's company. Add --json for machine-readable output. | Yes |
| codepraxis --edit ID | Update question metadata and return a preview container URL. Add --open to launch it. | Yes |
| codepraxis --install claude-plugin | Install 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.
| Variable | Purpose |
|---|---|
| CODEPRAXIS_TOKEN | API key. Skips the login prompt, the CI path. |
| CODEPRAXIS_API_URL | Platform base URL. Defaults to production. |
| OPENAI_API_KEY | Lets model-dependent cases run in --local instead of reporting unverifiable. |
| OPENAI_BASE_URL | An OpenAI-compatible endpoint, if not the default. |
| What you see | What 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 edit | Publishing 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. |