If you run a coding agent on a real project, you have probably lived through all three of these moments.
Moment one. The agent runs your test suite. The terminal answers with thousands of lines — progress dots, a coverage row per module, missing-line lists, warnings, stack traces. The agent dutifully reads it. You watch a five-figure chunk of context evaporate on formatting noise. Then the fix loop iterates, and it happens again. And again.
Moment two. The agent reads that wall and cheerfully reports that everything passed. It did not. A collection error killed the run before a single test executed — nonzero exit code, but no FAILED lines anywhere. Nothing looked failed, so the agent called it green.
Moment three. The check finally goes green — because the agent quietly lowered the coverage threshold. Or deleted the inconvenient test. Technically, you did ask it to make CI pass.
Three different failures, one root cause: in most agent loops, verification is text interpretation. The model reads terminal output and decides, from prose, whether the run looks green. That decision is expensive every time, wrong occasionally, and gameable always.
I kept hitting all three on my own projects, got tired of it, and built ckdn. It hit v1.0.0 this week. This post is the why and the how — in the order of those three moments.
Why "checkdown"?
It's a World Cup year, so forgive one football metaphor — wrong football, right idea. In the American game, a checkdown is the short, safe pass a quarterback throws instead of forcing a risky long play.
That is exactly this tool's job. Instead of hurling an entire terminal log into the model's context and hoping the long ball connects, ckdn hands the agent a short, safe, catchable result. The package name is shortened to ckdn; the idea stays the same:
Let agents move fast. Give them a safe verification path.
Moment one, solved: digests instead of logs
ckdn sits between the agent and your verification tools. Every configured check runs through one orchestrator that executes the real subprocess, archives the complete output as evidence, parses the machine-readable report, and emits a* bounded, deterministic digest* — the only thing the agent reads.
On one of my projects, a single successful test-plus-coverage run produced roughly 14,000 tokens of terminal output. The useful conclusion was four lines:
4250 tests passed
coverage: 99.18%
required: 99%
status: pass
Here is what a passing check looks like through ckdn — this is the complete stdout:
{
"check": "pytest",
"rc": 0,
"run_dir": ".agent-runs/20260710T173400Z-pytest",
"schema": "ckdn.digest/2",
"status": "pass",
"summary": {
"counts": {
"tests": 144
}
}
}
Two orders of magnitude cheaper — and the fix loop pays this price per iteration, so the savings multiply. Green results stay tiny. Failures carry bounded evidence: top findings with locations and snippets, counts, explicit truncation counters. The full log stays on disk for the rare case someone actually needs it.
Moment two, solved: exit code and evidence must agree
The false-green story from the intro is not hypothetical — it's the canonical failure of any "read the text and decide" verifier, and it's one format drift away at all times.
ckdn refuses it structurally, by combining two independent sources of truth:
- the actual subprocess exit code, owned by the orchestrator;
- evidence extracted by a format-aware parser — JUnit XML, coverage XML, JSON reports; machine-readable artifacts wherever the tool offers one, terminal text only as a last resort and then with drift guards.
The two must agree before a result becomes pass.
ckdn may downgrade green. It never upgrades red.
Every run ends in exactly one of four states:
| Status | Meaning |
|---|---|
pass |
Exit code, parser evidence, and policy gates all agree |
fail |
A normal check failure — or a policy gate failure (e.g. coverage below fail_under) |
error |
The command itself broke: collection error, missing binary, timeout. Fix the run, not the code |
parse_mismatch |
The command returned green, but the evidence contradicts it. Green is untrusted |
parse_mismatch is the status most tools don't have and the reason this one exists. When the exit code says "fine" and the JUnit report says "one failure", something is wrong — with the tool, the parser, or the invocation — and the right answer is a loud alarm, not a silent verdict either way.
A parser never decides the final status. It reports facts; a separate reconciliation layer owns the verdict, and contract tests pin its invariants: a nonzero exit code can never become pass; contradictory evidence invalidates a zero exit code; parser drift becomes a loud error, never a quiet clean.
Moment three, solved: the repository owns the checks
ckdn is not an arbitrary shell handed to an AI agent. Checks are declared by the repository in ckdn.toml:
[check.pytest]
command = "uv run pytest -q --junitxml {run_dir}/junit.xml"
parser = "pytest"
[check.coverage]
command = "uv run pytest -q --junitxml {run_dir}/junit.xml --cov=src --cov-report=xml:{run_dir}/coverage.xml"
parser = "coverage"
fail_under = 96.0
[check.lint]
members = ["ruff"]
...
The agent can ask ckdn to run pytest or lint. It cannot invent a new command, weaken the coverage threshold, sneak in a convenient shell pipeline, or silently redefine what "verified" means. Commands run without a shell at all —tokenized, no pipes, no && —because a pipeline is exactly where exit codes get laundered.
The threshold that moment three's agent lowered? In ckdn it's a policy gate in the config, checked against the coverage XML itself. Making it pass dishonestly now requires editing a file the diff will show — not a quiet reinterpretation of prose.
The repository owns the checks. The agent only triggers them.
Try it in two minutes
Python ≥ 3.11. The core CLI is stdlib only — zero third-party dependencies.
uv tool install ckdn
cd your-project
ckdn init # writes a starter ckdn.toml
echo '.agent-runs/' >> .gitignore
ckdn run pytest # one atomic check, digest on stdout
ckdn run lint # an alias running its members in order
ckdn show # pretty-print the latest digest
Every run gets its own evidence directory — compact output does not mean lost information:
.agent-runs/
20260710T173400Z-pytest/
full.log # complete interleaved output
junit.xml # machine-readable tool report
meta.json # argv, rc, timestamps, duration, log sha256
digest.json # deterministic facts for the agent
The digest is deterministic on purpose — no timestamps, no durations (those live in meta.json). Identical results produce identical bytes. The digest is compact; the evidence is complete.
MCP support
For agents that should call ckdn over MCP instead of shelling out, there's an optional server (the core stays dependency-free; FastMCP is an extra):
uv tool install 'ckdn[mcp]'
claude mcp add --scope project ckdn -- ckdn-mcp # Claude Code; Cursor/Codex configs in the README
Six tools: list_checks, run_check, run_group, get_digest, list_runs, get_evidence. The same trust rules apply: only configured checks, no arbitrary shell, bounded evidence access, and — importantly — fail / error /parse_mismatch are normal structured results, not MCP protocol errors. A red check is information, not an exception.
MCP is a transport, not a second implementation of the verification semantics: the CLI and the server share one application layer.
Parsers
v1.0.0 ships parsers for pytest, coverage, Ruff (lint and format), ty, mypy, Pyright, Black, pip-audit, Bandit, Pylint,any SARIF-producing tool, and generic exit-code-only checks.
The SARIF and JUnit parsers matter more than the list suggests: they are format parsers, not tool parsers. Anything that writes SARIF (semgrep, gitleaks, trivy, …) or JUnit XML (most test runners in most languages) works today, even though ckdn itself is written in Python. The parser API is public and small — a parser reports evidence and never gets to reinterpret a red exit code as success:
from ckdn.parsers.base import Finding, ParseContext, ParseResult
class MyToolParser:
name = "mytool"
def parse(self, ctx: ParseContext) -> ParseResult:
report = ctx.artifact("report", "mytool.json")
if not report.exists():
return ParseResult(parser_ok=False,
notes=[f"report not found: {report}"])
return ParseResult(findings=[...], summary={"count": 0})
Why not just ask the model to summarize the log?
Because summarization and verification are different jobs.
A language model is genuinely good at explaining a failure. It should not be the authority on whether the command succeeded — that authority has to be boring, deterministic, and testable. The split that works:
ckdn establishes what happened. The agent decides what to do next.
What was the real exit code? Was the expected report produced? Could it be parsed? Does the evidence contradict the process result? Did a policy gate fail? — deterministic layer. How to fix it — that's the agent's job, and it does that job better when the input is twenty findings with locations instead of a scrolling terminal.
What ckdn is not — and where it won't help you
Honest scoping, so you don't install the wrong tool:
- It is not a CI platform, a task runner with shell access, an AI log summarizer, a dashboard, or an LLM judge. It replaces none of pytest, Ruff, coverage — it wraps them.
- If you're coding without agents and happily reading your own logs, ckdn solves a problem you don't have.
- The built-in parser set is Python-ecosystem-first today. Other stacks enter through SARIF, JUnit, or the generic parser — a dedicated
cargo testoreslintparser is a config recipe or a small class away, but it's not in the box yet. - No parallel execution, no watch mode. It's a verification boundary, not an orchestra conductor — sequencing stays with the caller.
- It's a young v1.0.0 by a solo maintainer. The status-model invariants are contract-tested and I run it on my own projects daily, but the API surface outside the digest schema may still move.
One job, done strictly: run configured checks and return a result the agent does not have to guess about.
Try to break it
ckdn is open source under MIT:
- Repository: github.com/orenlab/ckdn
- PyPI: pypi.org/project/ckdn
I would especially value feedback from people running coding agents on real projects:
- Which of the three moments from the intro do you hit most?
- Which parsers are missing for your stack?
- Where does the configuration feel awkward?
- Can you produce a case that ckdn misclassifies? That last one is the bug I want most.
Try it, break it, open an issue. And if the idea resonates, a star helps other people find it.
Top comments (0)