This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
jwt-toolkit is a defensive Click-based CLI for inspecting, verifying, auditing, forging (for self-audit), and brute-forcing weak HMAC secrets on JSON Web Tokens. It is published to PyPI and installed by external users — treat it as a public-API library wrapped in a CLI, not as an internal script.
Threat-model framing. audit, forge, and crack exist so an operator can find weaknesses in tokens and systems they own before an attacker does. Every user-facing string — help text, error messages, panel titles — should reinforce that posture. Output is meant to reflect the security outcome, not the technical event ("Signature did not verify — token is not trustworthy", not "HMAC mismatch at byte 0x20"). Never echo a user's --secret or private key in messages, banners, debug output, or exceptions.
- Python 3.13+ only. No backcompat shims, no
__future__imports. - Package manager:
uv. Lockfile isuv.lock— commit changes to it whenever you touch dependencies. - Build backend:
hatchling. Entry point:jwt-toolkit = "jwt_toolkit.cli:cli". - All workflows go through the
Makefile— prefermake <target>over invoking tools directly so config (severity levels, parallelism, coverage threshold) stays consistent.
make install # uv sync --group dev + pre-commit install
make fmt # ruff format + ruff check --fix
make lint # ruff format --check + ruff check (no fixes)
make typecheck # mypy (strict mode)
make test # pytest --cov -n auto (coverage gate: 80%)
make test-fast # pytest -n auto, no coverage
make security # bandit (medium+)
make audit # pip-audit
make check # lint + typecheck + test + security + audit ← full CI gate
make pre-commit # pre-commit on all filesBefore declaring non-trivial work done, run make check. It mirrors CI; if it passes locally, CI will pass. Don't skip stages, don't pass --no-verify, don't add # type: ignore or # noqa without surfacing it to the user.
Single test: uv run pytest tests/test_cmd_decode.py::test_name. Run one file with coverage off (make test-fast -- tests/test_cmd_decode.py) when iterating.
jwt_toolkit/
├── cli/ # Click plumbing only — banner, console, panels, decoding helpers.
├── commands/ # One file per subcommand. THIN. Parse flags, call core, render via console/panels.
└── core/ # Pure logic. NO Click, NO Rich, NO I/O beyond explicit file/HTTP helpers.
# See jwt_toolkit/core/CLAUDE.md for crypto invariants.
Commands stay thin: they translate flags → core inputs, call into core/, then format results. New logic belongs in core/. If you find yourself importing click or rich inside core/, stop — that's a layering violation.
Three things are observable by downstream users and break things when you change them silently:
- The CLI grammar — command names, flag names, flag semantics. Renames or removals are breaking changes; add an alias before removing, or wait for a major version.
--jsonoutput shape. Every command that supports--jsonincludesschema_version(currently"0.1", defined injwt_toolkit/cli/decoding.py). If you change a field name, type, or remove a field, bumpJSON_SCHEMA_VERSIONand call it out in the PR. Adding new optional fields is non-breaking.- Stable error codes —
core/errors.pyexceptions carry a machine-readablecode.--jsonconsumers key off these. Codes are forever; pick a new one rather than reusing or renaming an existing one.
Exit codes follow Click defaults: 0 = success, 1 = command-handled failure (click.ClickException), 2 = usage error (Click's own). Don't sys.exit(N) directly; raise click.ClickException so the message is rendered through the project's console.
- Never
print(). Usefrom jwt_toolkit.cli.console import console(andclick.echofor--jsonpayloads).print()bypasses--quiet,--no-color, and theJWT_TOOLKIT_QUIET=1/NO_COLOR=1env vars. - Global flags
--no-banner,--quiet,--no-colorexist for scripting. Any new command must respect them. - Secrets and keys: accept them as
--secret/--private-key. Never accept secrets as positional arguments (they show up in shell history andps). Don't echo their values in errors — refer to them by flag name only. - Errors are
raise click.ClickException("user-facing sentence")from the command layer; deeper layers raise domain exceptions fromcore/errors.py. The CLI translates the latter into the former. - Match the help-text voice of existing commands: one short security-outcome sentence, no implementation detail.
- When registering a new command, add it to the
Commands:block injwt_toolkit/cli/__init__.py'sCLI_HELPwith the same column alignment as the existing entries.
- Plain single-line
#comments only. No banner/divider blocks, no section headers in comments. - Comments are rare. Write one only when the why is non-obvious (hidden constraint, RFC reference, workaround for a specific bug). Don't explain what — names and types do that.
- Ruff is the source of truth: line length 100, Python 3.13 target, the curated ruleset in
pyproject.toml. Don't argue with it; runmake fmt. - mypy runs in strict mode. New code adds type hints to every public function.
# type: ignorerequires a[error-code]and a reason.
xfail_strict = true— anxfailthat unexpectedly passes is a failure. Remove the decorator when fixing the underlying issue.filterwarnings = error— any warning fails the test. Don't suppress; fix the cause.- Coverage gate: 80%.
jwt_toolkit/cli/banner.pyis excluded (pure UX). Don't add new exclusions to dodge the gate. - Tests run in parallel (
-n auto) — no shared mutable state, no cwd dependence, no test-order coupling. - CLI tests live in
tests/test_cmd_*.pyand use Click'sCliRunner. Core tests live intests/test_<module>.pyand exercise pure functions without going through the CLI. - Every new command needs at minimum: happy path, error path, and
--jsonshape test if it supports--json. - No network in tests. JWKS-using code paths must use the offline fixtures in
tests/conftest.py/tests/helpers.py.
Version lives in pyproject.toml. Tag a release and publish via GitHub Releases — .github/workflows/publish.yml builds with uv build and uploads to PyPI via Trusted Publishing (OIDC, pypi GitHub environment with manual approval). Never publish manually with twine/uv publish. Full release flow is in the /release skill at .claude/skills/release/SKILL.md.
- @CONTRIBUTING.md — branch naming, commit conventions, PR checklist, full contributor flow.
- @jwt_toolkit/core/CLAUDE.md — crypto, parsing, and error-model invariants for the
core/package.