Configuration¶
ckdn.toml at the project root (ckdn init; override with --config).
Subprocesses and relative runs_dir paths resolve from the invocation
working directory (--cwd or CKDN_CWD), not from the config file's parent
— so a config copied to /tmp can drive checks in a git worktree.
Excerpt of the starter (the full catalogue is written by ckdn init):
[run]
runs_dir = ".agent-runs"
keep = 20
top = 20
max_snippet_lines = 12
log_tail_lines = 40
[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=term-missing --cov-report=xml:{run_dir}/coverage.xml"
parser = "coverage"
fail_under = 96.0
[check.mypy]
command = "uv run mypy --output json"
parser = "mypy"
format = "json"
[check.ruff]
command = "uv run ruff check --output-format json --output-file {run_dir}/ruff.json ."
parser = "ruff"
[check.lint]
members = ["ruff"] # add pylint / bandit / … when enabled
[check.style]
members = ["format", "ruff"] # format + lint atomics
Checks¶
Atomic check: command + parser (required), optional timeout in
seconds (a timeout yields rc=124 and a non-green status). Any other key is
passed to the parser as an option (fail_under, score_fail_under,
fail_levels, …).
Set a timeout on long checks
timeout is optional and unset means wait indefinitely. A tool that
hangs then hangs the check. ckdn will not leak the process tree — it is
terminated on interrupt — but only a timeout bounds an unattended run,
so set one on anything that talks to the network or runs a full suite.
Runs are serialized per (runs_dir, check): starting a second ckdn run of
the same check in the same workspace is refused while the first is alive,
rather than doubling the load on the same tools. A lock left by a dead process
is reclaimed automatically, and the run that reclaims it records a note saying
the previous run did not exit cleanly and may have left processes behind.
That note is advisory: it describes the previous run, so it never changes this run's status. ckdn does not stop leftover processes for you. Only its own pid is ever recorded — never the child's process group — and a pid can be recycled, so there is no target it could act on without risking an unrelated process. If a run behaves oddly right after such a note, look for leftovers from the previous one.
Optional env (a table of string values) is overlaid on the subprocess
environment for that check only — the inherited environment (PATH, …) is
preserved. {run_dir} is substituted in env values, so a tool can be pointed
at the run directory. env is never written to meta.json.
[check.coverage]
command = "pytest --cov=src --cov-report=xml:{run_dir}/coverage.xml"
parser = "coverage"
env = { COVERAGE_FILE = "{run_dir}/.coverage", PYTHONWARNINGS = "error" }
Alias: members = ["atomic", …] only (optional fail_fast). No nesting.
See Aliases & aggregates.
Commands are tokenized with shlex and run without a shell — no pipes, no
redirects, no &&. Deliberate: a shell pipeline is exactly where exit codes
get laundered (cmd | tee reports tee's status). If a check needs shell
features, wrap them in a script and point command at it. {run_dir} is
substituted in commands and artifact paths — point machine-readable reports
into the run directory.
Command policy¶
Default workspace: before any subprocess starts, path-like argv tokens must
resolve inside the invocation cwd (--cwd / CKDN_CWD). /etc/passwd,
.. escapes, and paths under /etc, /proc, ~/.ssh, etc. are rejected.
MCP extra_args are subject to the same rules.
- Set
command_policy = "allowlist"to require configured command prefixes (uv run,uvx, …, or custom[run.command_allowlist].prefixes). - Use
command_policy = "off"only for exotic workflows.
In CI, ckdn lock-config then ckdn verify-config --locked catches tampered
commands without running them.
Pre-flight diagnostics¶
ckdn doctor runs static, deterministic checks over ckdn.toml before any
subprocess, so a misconfiguration surfaces as an actionable message instead of
a confusing runtime error ("report not found"). It reports two levels:
- error — a run that cannot possibly work: the command's executable is not
on
PATH, or the command is empty / not tokenizable. - warning — a likely mismatch between a command and its parser: a
file-based parser (
pytest,coverage,ruff,bandit,pip_audit,pylint,sarif) whose command never writes the report it will read, or a flag a parser expects (mypy --output json,pyright --outputjson,reformat --check).
$ ckdn doctor
error: [ghost] executable not found on PATH: totally-not-installed
warning: [pytest] the pytest parser reads `junit.xml` from the run dir, but
the command never writes it — add the flag that emits `{run_dir}/junit.xml`
1 error(s), 1 warning(s)
Exit code is 1 on any error (or on warnings too with --strict), else 0 —
so it drops into CI as a config gate. Diagnostics are advisory heuristics; they
never run a command and are separate from the status model.
Working directory¶
Subprocesses and relative .agent-runs/ resolve from cwd, not from where
ckdn.toml lives.
- CLI:
--cwd/CKDN_CWD. - MCP: per-call
cwdon every config-using tool, orCKDN_CWD/ckdn-mcp --cwdas server defaults.
When the config file is outside the project tree (worktree, temp config), pass the project root as cwd on every run — otherwise tools execute in the wrong directory.