Skip to main content

kragg

kragg is an opinionated guardrails framework for AI-assisted Python projects. It is agent-first: coding agents (Claude Code, Codex, Cursor, Gemini CLI, …) are the primary users, running check → fix loops. kragg gives them one command, structured results, meaningful exit codes, and copy-pasteable fixes.

Install modes

Dev dependency (recommended). kragg runs inside the project environment, so every tool sees the project's packages:

uv add --dev kragg
uv run kragg check

Global tool (pipx / uv tool). kragg runs from its own environment, but environment-dependent gates (pytest, mypy, pip-audit, deptry) are always executed on the project's interpreter, resolved in this order:

  1. KRAGG_PROJECT_PYTHON environment variable (explicit override)
  2. <project>/.venv
  3. an active VIRTUAL_ENV (ignored when it is kragg's own environment)
  4. uv run --project <project> as a fallback

If no project environment exists, those gates fail loudly with the exact fix (uv sync, uv add --dev mypy, …) instead of silently running in the wrong environment. kragg doctor shows which interpreter was resolved and which tools are missing.

Commands

kragg new my-app --kind cli   # layered skeleton (cli | api | worker | mcp) + uv sync
kragg new my-mcp --kind mcp   # MCP server: fastmcp (recommended) with an HTTP
                              # /health route + in-memory Client test;
                              # --mcp-sdk official uses Anthropic's mcp package.
                              # Servers bind 127.0.0.1:8000 by default and read
                              # HOST/PORT env vars: PORT=8931 uv run my-mcp
kragg new mcp --package my_pkg  # keep a project name whose derived package would
                              # shadow an installed import (kragg new refuses
                              # names that shadow the stdlib or common packages;
                              # --allow-shadowing downgrades that to a warning)
kragg gen module payments     # service/domain/test slots in the layout
kragg init .                  # add guardrails to an existing project
kragg check                   # all gates, consolidated report
kragg check --changed         # only files changed in git (cheap inner loop)
kragg check --since main      # changed vs merge-base with a ref
kragg check --file src/foo.py --file src/bar.py
kragg check --format json     # stable machine-readable schema
kragg fix                     # auto-fix formatting and safe lint
kragg map                     # public symbol inventory (what already exists)
kragg spec                    # test suite rendered as a readable spec tree
kragg coverage                # uncovered lines in critical functions (ranked)
kragg mutation                # mutation-test changed critical files (cosmic-ray)
kragg mutation --all          # mutation-test every critical file
kragg mutation --update-baseline  # accept current survivors (equivalent mutants)
kragg brief                   # reviewable digest of the change set
kragg security
kragg audit
kragg criticality --write     # call-graph risk -> CRITICALITY.md + .kragg/
kragg status                  # what failed last run, without re-running
kragg flaky                   # gates that flipped pass/fail on an unchanged commit
kragg flaky --rerun 20        # re-run the suite N times, rank tests by failure ratio
kragg hook claude             # harness hook adapter (reads hook JSON on stdin)
kragg doctor                  # environment diagnostics with exact fixes
kragg policy show

Gates

Fast (always all run): ruff, mypy, typing-strictness (the project's mypy config actually meets the strict floor — no missing/loosened flags, no ignore_errors, no bare # type: ignore; makes strict-ai-python a verified contract instead of a label), radon-cc, radon-mi, halstead, type-complexity, boundaries (layered import contract from [tool.kragg] layers), structure (file/symbol budgets), forbidden-calls (project-banned APIs from [tool.kragg.forbidden_calls], resolved through imports and annotations), nullable-default (arithmetic on .get(key, default) results, which crash when the key is present-but-null), masked-recovery (a session.commit()/.flush() inside a handler catching a DB failure, not wrapped in its own try — the write raises over the dead session and the secondary error is what surfaces, masking the root cause; rollback/close stay legal), critical-tests (critical functions cannot change without test changes), test-quality (no assertion-free tests; critical functions must be referenced by tests), secret-default (secret-named env reads, fields, and parameters given silent fallback defaults — a blank signing key must fail at startup, not sign), bandit, detect-secrets. Slow (skipped while fast gates fail): pytest+coverage, critical-coverage (public critical functions must have no uncovered lines), pip-audit.

Test depth

Green checkmarks are easy to fake, so kragg looks past them with layered, mostly-deterministic signals — each a harder-to-game answer to "do the tests actually defend behavior":

  • what's runkragg coverage surfaces uncovered lines in critical functions, ranked by fan-in, instead of a gameable global percentage; the critical-coverage gate fails on any uncovered line in a critical function.
  • what's defendedkragg mutation runs cosmic-ray over the changed critical files (the criticality graph and git diff keep it cheap) and reports surviving mutants as file:line fixes. Accept equivalent mutants with kragg mutation --update-baseline.
  • what's claimedkragg spec renders test names and docstrings as a documentation tree and flags critical functions that have only example-based tests (property-based tests, via Hypothesis, kill more mutants).
  • what's trustworthykragg flaky mines the run journal for gates that flipped on an unchanged commit; --rerun N re-runs the suite and ranks tests by failure ratio (for cron/CI, never the inner loop).

Mutation and active flaky runs are deliberately outside kragg check: they are on-demand or CI surfaces, not inner-loop gates.

Security contracts

No generic scanner can infer a repository's unstated security requirements — that request bodies must be bounded, that this codebase never shells out directly. What it can do is enforce them mechanically once a human states them. That is the forbidden-calls gate's job: build the safe wrapper, then ban the raw API repo-wide with a fix hint that names the wrapper —

[tool.kragg.forbidden_calls]
"starlette.requests.Request.body" = "read bodies via app.http.read_limited_body(request, limit=...)"
"subprocess.run" = "run external commands through app.runner.run_command"

Every resolvable call to a banned path fails with the hint as the fix; the wrapper's own implementation is the one legitimate call site, marked with a trailing # kragg: ignore so the exemption is visible in review. An entry bans everything beneath it (pickle bans pickle.loads; a class path bans every method). Resolution is deterministic and annotation-driven — it catches await request.body() on a request: Request parameter, which import-based banned-API linters cannot. Re-exports are distinct names: banning the starlette path does not ban fastapi.Request.body — list every path the project imports.

secret-default closes the inverse hole to detect-secrets: not a credential that is present in the repo, but one that is absent at runtime and silently defaulted. kragg's own repo dogfoods the contract — raw subprocess.run is banned in favor of kragg.runner.run_command.

masked-recovery guards the incident-response path itself: an except handler for a DB failure that commits or flushes over the session that just died raises a NEW exception from inside the handler, and that secondary failure is what reaches the logs and the pager — the root cause is masked exactly when you need it most. The gate flags the un-protected write on its own line; rollback()/close()/aclose() are the calls designed to be safe on a dead session and never flag, and recovery_allowlist adds a repo's own teardown helpers on top of (never instead of) those three.

kragg mutation runs cosmic-ray on the project interpreter, so add it to the project being checked (uv add --dev cosmic-ray); kragg prints the exact command if it is missing. Property-based tests use Hypothesis (uv add --dev hypothesis). Mutation needs an editable/source install so it mutates the code the tests import.

Agent-native design

Agents drift where they have freedom, so the scaffold removes the freedom: every kind ships one layered layout (entrypoints/services/domain/) with the boundaries gate enforcing dependency direction from the first commit, and kragg gen module creates new code in the one place it belongs. The tool holds the memory the agent lacks: kragg map is the inventory of what exists (injected at session start so nothing gets reinvented), .kragg/history.jsonl remembers runs, CRITICALITY.md remembers risk, and kragg brief renders the change set legible to the human reviewer.

Agent loop design

  • kragg check runs all static gates and reports every failure in one run (one loop iteration instead of one per gate). Slow gates (pytest, pip-audit) are skipped while static gates fail; --fail-fast and --all override.
  • Output is token-efficient: violations are deduplicated, capped per gate (--max-violations, policy max_violations_per_gate), and reported as file:line pointers with fix hints instead of raw tool dumps.
  • Exit codes are branchable without parsing: 0 pass, 1 gate failures, 2 usage error, 3 environment broken.
  • Every run appends a slim line to .kragg/history.jsonl; kragg status answers "what failed last run" for a few tokens.
  • Scaffolding emits AGENTS.md as the canonical agent contract (read by Codex, Cursor, Gemini CLI, and Claude Code via a CLAUDE.md pointer), plus hooks that run kragg check --changed after edits.

Configuration

Configure kragg in pyproject.toml under [tool.kragg], or in a standalone kragg.toml (top-level keys, takes precedence when present):

[tool.kragg]
profile = "strict-ai-python"
source_paths = ["src"]
test_paths = ["tests"]
coverage_fail_under = 80
type_max_nesting_depth = 2
type_max_length = 40
max_violations_per_gate = 25
max_file_lines = 500
max_public_symbols = 20
structure_exclude = [
    # flat or generated files that legitimately exceed the budgets;
    # document why each entry earns its exemption
    "src/app/icons.py",  # 4k lines of generated base64 icon data
    "*_pb2.py",          # protobuf-generated modules, any depth
]
mutation_include = [
    # explicit mutation targets (globs); REPLACES criticality as the base set,
    # to reach high-value logic that modest fan-in keeps out of the graph
    "src/billing/entitlement_engine.py",
    "src/billing/*.py",
]
mutation_exclude = ["src/observability.py"]  # fail-safe glue; mutants mostly equivalent
recovery_allowlist = [
    # methods masked-recovery additionally allows inside DB-failure handlers;
    # ADDS to the builtin rollback/close/aclose, never replaces them
    "safe_teardown",
]
secret_name_suffixes = [
    # names treated as secrets by secret-default (suffix or exact bare match);
    # bare "_key" is deliberately absent: sort_key/cache_key are not secrets
    "_secret", "_token", "_password", "_passphrase", "_api_key",
    "_signing_key", "_secret_key", "_private_key", "_access_key",
]

[tool.kragg.forbidden_calls]
# dotted API path -> the fix an agent should apply instead
"subprocess.run" = "run external commands through app.runner.run_command"

structure_exclude exempts matching files from the structure gate's file- and symbol-budgets only — they stay subject to every other gate, so the cap stays meaningful repo-wide. Reach for it after splitting fails: genuinely flat data, vendored code, or generated modules you can't annotate. Patterns are repo-root-relative POSIX paths matched with fnmatch (case-sensitive); * matches any characters including /, so src/generated/* exempts that whole subtree — scope patterns narrowly.

mutation_include / mutation_exclude scope kragg mutation (same glob convention). Mutation scope is its own axis, not just criticality: by default it targets the files defining any critical function — public or private (it mutates whole modules, so a high-risk private boundary like Client._call still counts). mutation_include replaces that with an explicit set — reach for it to mutate high-value logic (access checks, billing math) that modest fan-in keeps out of the criticality graph. mutation_exclude drops files even when critical: fail-safe glue like telemetry, where try/except: return swallowing makes most mutants equivalent, so a high survival rate there is expected, not a test gap. Ad-hoc override: kragg mutation --all --path "src/billing/*.py" (repeatable).

V1 scope

kragg models agentic workflow support as policy packs, not as an LLM runtime. A policy pack defines the gates, thresholds, generated agent instructions, and project wrappers that keep AI-authored Python code inside known constraints.

The default policy pack is strict-ai-python.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kragg-0.9.0.tar.gz (237.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kragg-0.9.0-py3-none-any.whl (89.7 kB view details)

Uploaded Python 3

File details

Details for the file kragg-0.9.0.tar.gz.

File metadata

  • Download URL: kragg-0.9.0.tar.gz
  • Upload date:
  • Size: 237.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for kragg-0.9.0.tar.gz
Algorithm Hash digest
SHA256 c7f6c5dfe4478da38e201eb7c36902790f88ebd2e90279aff0f1aebfa7d5ce28
MD5 93294f954db7d43d63b8bb1528078c6d
BLAKE2b-256 11d48e930afce9411347dc8779fec06491b7639be9d162054341233aceb3f9c1

See more details on using hashes here.

File details

Details for the file kragg-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: kragg-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 89.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for kragg-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a93ccabc76ca4603770511e60cdcc0d7afe4ce19593b869efc2160ba480e62ca
MD5 880e1bebde77ccc70b6197e99d76312c
BLAKE2b-256 2a1569b3ed2c7af9446da4f612070dea0685922de1f969ef9116b72032d4644d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page