Imported from MisterTK/keel (
packaging/claude-skill/keel/SKILL.md). Install upstream withnpx skills add MisterTK/keel --skill keel. Copyright stays with the author.
Keel
Keel is "the SQLite of durable execution": resilience (retry, backoff,
timeout, circuit breaker, rate limit, cache, poll-until-terminal) and
opt-in crash-resumable durable flows, applied at the call sites a target
project already makes — zero code changes. Policy lives in one file,
keel.toml. There is no service to run, no database to provision, and no
daemon.
Is this project already using Keel?
Check for either signal before assuming a fresh install:
- A
keel.tomlat the project root. - A
## Keelsection inAGENTS.md(written bykeel init --agents).
If either is present, treat Keel as already adopted — go straight to "Working in a Keel-adopted project" below. Do not re-run install steps or suggest ad hoc retry code; both signals mean the ground rules there already apply.
Adding Keel to a project
Pick by language. The library (imported/depended on at runtime) and the
keel CLI (run/doctor/init/status/mcp/…, a devtool) are always
separate packages — install both together for the full experience, or the
library alone to stay lean:
# Python
pip install keelrun keelrun-cli # library + CLI in one line
# or just the library:
pip install keelrun
# Node (>= 22.5)
npm install keelrun keelrun-cli # library + CLI in one line
# or just the library:
npm install keelrun
# Rust — cargo has no single command spanning both operations
cargo add keelrun --rename keel # library: #[keel::wrap]
cargo install keelrun-cli # CLI binary
# Just want the CLI, no persistent install, any language:
uvx --from keelrun-cli keel run app.py
Then, from the project root — observe first, then write policy:
keel run <entry> # zero config; every run records real traffic into .keel/discovery.db
keel init # writes keel.toml from evidence: observed traffic, imports, call sites
keel doctor --json # the honesty report — what's covered, what isn't, why
keel init --agents # seeds the AGENTS.md section future agent sessions read
Static-only keel init (no observed runs) works but proposes from scan
evidence alone — prefer at least one representative run under keel run
(or keel record run, which also captures a replayable fixture) first.
keel init never overwrites blindly — re-run keel init --diff any time to
preview what evidence would add or remove before touching the file.
Known gaps, so as not to overpromise: Rust has no keel init --rust
static-scan support yet (add the crate and call keel::init() yourself —
see crates/keel/README.md if working in this repo, or the published
crate's own README otherwise); a cargo-keel subcommand does not exist.
Working in a Keel-adopted project
- Before changing anything resilience-related, run
keel doctor --jsonto see what's wrapped, what's visible-but-unwrapped and why, and any findings (including, where built, a check for pre-existing retry/backoff code that might now be redundant with Keel's own). - Never hand-write a retry loop, backoff decorator, or manual circuit
breaker around a call Keel already wraps — edit
keel.tomlinstead.keel doctorwill flag known resilience libraries (e.g.tenacity,backoffon Python) still present so they don't silently compound. - Propose policy changes as a diff, not a guess:
keel init --diff --jsonshows exactly what evidence would add or remove. - Every command has a deterministic
--jsontwin (sorted keys, no timestamps) — diff two calls to see real change, don't parse prose. keel explain <KEEL-E0NN>gives the exact what/why/next for an error code without needing a web search.- Uninstalling Keel (removing the package) restores the original behavior exactly — there is nothing else to revert.
Reading what Keel did
Four levels of evidence, cheapest first — reach for keel doctor/keel status --json for structured diagnosis (see the protocol below), but for a
human-facing "what happened" check these first:
- Console summary — always on, no CLI needed. Every run under Keel
(
keel run,python -m keel run, the Node loader,#[keel::wrap]) prints one summary to stderr at exit, e.g.keel ▸ 47 calls · absorbed 3 rate limits · 2 retries succeeded · 4 calls unprotected, plus a hint line pointing atkeel report --open(or, if the CLI isn't installed, theuvxequivalent). A no-op run stays silent.console = falseunder[telemetry], orKEEL_QUIET=1, turns it off. In a container stderr is the surface that survives — a parent that captures a child's stderr silently swallows it — andKEEL_LOG_FORMAT=jsonmakes that summary, the startup line, and any activation error one JSON object per line. - Which backend ran — check this before trusting any timing. The startup
line and the activation JSON name the backend, and
KEEL_BACKEND=auto(the default) falls back from the native core to a pure-Python one whenever the native module cannot be imported. That fallback is silent otherwise, and it costs durable flows and cross-run cache persistence, so the banner says so:(pure-Python backend: no durable flows, no cross-run cache). Since 0.6.5 the fallback honors wall time like the native core does — before that it advanced a counter instead of sleeping, so retry backoff, rate limits and poll intervals were not real. If you are diagnosing "the policy did not seem to apply", establish the backend first. keel report— a self-contained HTML page at.keel/report.html: per-target tables, a calls/failures trend, the newest run's event stream, and flow status.--openlaunches it;--jsonprints the same evidence as byte-deterministic JSON. Works with no persistent CLI install viauvx --from keelrun-cli keel report --open.- Live view —
keel report --watchrewrites the file on an interval (no networking);keel report --serveruns a loopback-only server the page polls (rejects a mismatchedHostheader). Use--watchwhen driving a long test run locally and want the file to stay current; use--servewhen someone else needs to watch the same page open. - Not logs, not remote. OpenTelemetry export is spans and metrics only,
needs a source build with the
otelfeature (published wheels and the npm addon do not include it), and never reaches a logging backend..keel/evidence (status,trace,report) is host-local and ephemeral on serverless platforms.
Evaluating Keel against a codebase (the protocol)
When asked whether/how Keel should cover a project — a fresh adoption or an audit of an existing one — do NOT stop at grepping for HTTP libraries. Work the six phases in order; the static scan is evidence, not the verdict.
- Scope. Enumerate every process that does I/O, not just the entrypoint:
the main app, MCP servers in
.mcp.json, shell-script launchers, cron entries, anything reached viasubprocess/child_process/exec. Keel must be installed inside a process to see its traffic — a sibling process is a coverage boundary, not a detail. - Explore. For each URL/host the code touches, trace how the request is
actually dispatched — which library sends the bytes (an SDK may wrap a
transport Keel adapts, or hide one it doesn't). Note stdlib transports:
Python's
urllib.requestis adapted (wrapped like any registry library);http.clientand rawhttp/httpson Node are seen in the scan but not adapted yet. - Collect. Run
keel doctor --json(or theget_doctor_reportMCP tool). Readtopologyfirst — every sighted host lands in exactly one ofwrappable("wrap it"),unreachable("can't reach it, here's why"), orexcluded("shouldn't reach it — seen only in dependency-averse gate files, a local/loopback host, an RFC 2606/5737 reserved name such asexample.com, or a host seen only in test files; the dependency-averse kind is deliberate and overridable with# keel: include, loopback and reserved names are fixtures by definition, and a test-only host needs policy only if production code reaches it too"), plusexternal_processesfor the sibling-process blind spots (test-file launches are counted separately; Python children that inherit the env are reported as self-activating rather than as blind spots). Then workfollow_upsstrictly top-down: it is ranked with rank 1 = the claim Keel is least able to verify itself (an unattributed URL) down to mechanical facts awaiting a decision. Codes are a closed set:url-no-transport,orchestration-blind-spot,subprocess-blind-spot,dependency-averse-excluded,local-host-excluded,reserved-name-excluded,test-only-excluded,preexisting-resilience,sdk-client-timeout,code-hash-stale. Then readboundaries— it names what this report could not parse (source languages, shell/Makefile/CI files,CLAUDE.md/AGENTS.mdgovernance prose) — andfindings, which carrieswarnitems that are not follow-up codes.runtime_activationsays whether this checkout has ever activated with this policy;journal-ephemeral-storagefires when[flows]meets SQLite in a container artifact. - Baseline before you mutate. Before proposing any behavior-changing
policy — retry, breaker, or a timeout that alters an outcome, as opposed to
a pure simplification swap like a poll loop →
pollpolicy — measure what actually fails. Wrap the candidate targets in observe mode (keel record run <entry>, or a[target]with no resilience knobs set — a no-knob wrap is pure passthrough plus events) and read the real failure-class distribution fromkeel status --json/ the event sink. Retry only helps genuinely-transient classes (conn/timeout/5xx/429); an auth or validation 4xx returns KEEL-E015 and is never retried, so wrapping it in retry buys latency, not resilience. Non-idempotent calls areKEEL-E014"observed, not retried" by default — confirm the transient hypothesis with evidence before recommending a behavior change. Two more codes worth recognizing:KEEL-E016means apollran out itsdeadlinewithout the response ever looking terminal — usuallyuntil.field/until.terminalnaming the wrong signal rather than a genuinely slow operation;KEEL-E017means a synchronous effect could not acquire its flow's step lock, whose likeliest cause is a nested call from another thread inside an open flow (a FastAPI/StarletteTestClientsuite is the classic shape). - Analyze & propose. Hunt hand-rolled resilience the scan may not flag
yet: retry loops with sleeps, poll-until-status loops,
mkdir-style mutexes, per-day guard files, broadexcept: return Noneswallows. When a validkeel.tomlis present, the firsthand-rolled-pollfinding attributed to an SDK poll call (per provider) carries an applyablefix(a route-keypollblock that beats the LLM host map for that route; Vertex:fetch*OperationPOSTs are judged idempotent since 0.6.0) — apply it withgit apply, then tuneinterval/deadline; later findings for the same provider carryfix_refinstead of afix— thefile:lineof the finding that actually holds the patch — rather than repeating it. Each is either replaced by policy (note whichkeel.tomlkey) or explicitly out of Keel's reach (say so honestly). Respect dependency-averse files — a stdlib-only gate/validator was built that way on purpose; never propose adding Keel as a dependency inside one. A shell-script orchestrator that builds its own at-most-once dispatch —mkdir/lockfile mutexes, guard files gating a retry, hand-rolled dead-PID checks around a launcher script — is out of the static scan's reach (it isn't Python/Node/Rust source) but is exactly whatkeel exec --flow <name> [--journal-file <path>...] -- <command>replaces: at-most-once dispatch per identity, crash-safe retry gating, and (with--journal-file) a declared- side-effect gate (KEEL-E033) before a failed run is retried. When the same subprocess call is launched from inside an already-Keel-active Python or Node process rather than a standalone shell script, prefer in-processcmd:interception instead — declare an argv match rule under[flows.match."cmd:<name>"]and Keel wraps the matchingsubprocess/child_processcall directly, nokeel execwrapper needed;keel doctor'ssubprocess-blind-spotfollow-up now cross-references any[flows.match]rule that already covers a sighted call. A durable flow refused with KEEL-E033 can be cleared once, out-of-process, withkeel flows force <flow-id>— a durable one-shot override, not a config change. Before resuming or trusting a durable flow's replay, checkcode_hash_staleinkeel flows --json/keel doctor --json—truemeans the entrypoint's resolved code changed since the flow's last run, so a replay would substitute steps recorded against a different program; confirm the flow should still resume before doing so. Finish withkeel init --diff --json/propose_policyand present the diff, never a hand-written policy guess. - Ship. The evaluation above is scoped to the repository; production
runs an artifact. Before declaring coverage, confirm the four deployment
invariants: (a)
keel.tomlis inside the image —keel doctor --jsonreportskeel-toml-not-in-imagewhen a root Dockerfile'sCOPY/ADDnever reach it; (b)KEEL_ENABLE=1reaches the process that does the I/O (a subprocess needs it in its env; Node children also needNODE_OPTIONS="--import keelrun/register"); (c)KEEL_CWD, if set, names the directory holdingkeel.toml— otherwise Keel refuses to activate and printskeel ▸ error: … Keel NOT activated; (d).keel/is on storage that survives a redeploy if durable flows are used. Then read the deploy logs for the one startup line:with policy <path>is proof,with production defaultsmeans the policy did not ship. SetKEEL_LOG_FORMAT=jsonin containers so that line and the exit summary are queryable fields. Locally,keel statusshows the last activation: language, version, the policy it loaded, and the pid.
Driving Keel via MCP
keel mcp starts a stdio JSON-RPC MCP server exposing six tools, each
byte-identical to its CLI --json twin:
| Tool | CLI equivalent |
|---|---|
get_status |
keel status --json |
get_doctor_report |
keel doctor --json |
propose_policy |
keel init --diff --json (an applyable diff, never writes) |
get_trace |
keel trace <flow> --json |
list_flows |
keel flows --json |
explain_error |
keel explain <code> --json |
get_doctor_report includes topology (the three honesty buckets) and a
ranked follow_ups list — work follow-ups top-down; rank 1 means Keel is
least able to verify the claim itself.
keel mcp has no --project flag — it always reports on its own
current working directory, so whatever launches it must set cwd to the
target project's root, not wherever the client process happens to start
from. Two config shapes, depending on whether keel is already on PATH:
Project-scoped, <project>/.mcp.json (Claude Code — launched with cwd
already at the project root, so no explicit cwd needed):
{
"mcpServers": {
"keel": {
"command": "keel",
"args": ["mcp"]
}
}
}
Global config (Claude Desktop's claude_desktop_config.json, or any client
that doesn't launch from the project directory) — cwd must be set
explicitly, or keel mcp reports on the wrong project or finds none at all:
{
"mcpServers": {
"keel": {
"command": "keel",
"args": ["mcp"],
"cwd": "/absolute/path/to/the/project"
}
}
}
If keel isn't installed globally (only via uvx), replace command/args
with "command": "uvx", "args": ["--from", "keelrun-cli", "keel", "mcp"] in
either shape above.