A configurable git commit/push gate installed as native git hooks — git commit and git push themselves are the gated commands, no second remote and no changed muscle memory.
Warden runs a policy-driven pipeline (lint, test, review, …) in a disposable worktree so a run never touches your live checkout, then fast-forwards your real branch and performs the push itself once everything passes. Policy is a set of stacking rules (match on branch, path glob, and a risk heuristic → overrides), and the pipeline is extensible with a typed subprocess SDK.
Built on axi-go (execution kernel — typed actions, effect-gated approval, tamper-evident evidence chain), fortify (resilience), statekit (policy visualization), and mcp-go (MCP surface).
make ci runs your checks — but in your dirty working tree ("passes locally,
fails CI"), and only when you remember to run it, leaving no trace.
Warden does what a Makefile can't:
- Runs clean. Every check runs in a disposable worktree seeded from the commit, so passing in warden means passing in CI — reproducibly.
- Can't be forgotten. Native
githooks fire automatically; no discipline required, no changed muscle memory. - Leaves proof. Each gated commit gets a hash-chained validation note that travels with the repo — so CI can trust it and skip re-running the checks, cutting minutes and cost (provenance-skip).
- Scales with risk. Rules match on branch / path / diff size, so heavy checks and human approval apply only where they matter.
It complements your checks rather than replacing them: point warden at the
commands you already run (warden import reads them from your Makefile, npm
scripts, lefthook, or CI).
warden is one static binary; pick whatever your machine already has — no Go toolchain required.
# npx — no install (works anywhere Node is present)
npx @klarlabs-studio/warden init
# curl (Linux/macOS)
curl -fsSL https://raw.githubusercontent.com/klarlabs-studio/warden/main/scripts/install.sh | sh
# Homebrew
brew trust klarlabs-studio/tap # first time only
brew install --cask klarlabs-studio/tap/warden
# Go devs
go install go.klarlabs.de/warden@latest # or: go run go.klarlabs.de/warden@latest initHomebrew refuses to load a cask from a third-party tap it has not been told
to trust, so the first install of anything from this tap needs
brew trust klarlabs-studio/tap once — per machine, not per tool.
On Windows: irm https://raw.githubusercontent.com/klarlabs-studio/warden/main/scripts/install.ps1 | iex.
The npx @klarlabs-studio/warden package is a ~15-line launcher: it ships the prebuilt binary
per platform (the esbuild pattern)
and execs it. All logic lives in the one Go binary; every channel above ships
that same binary.
Every self-fetched binary — the installer scripts and the version-pinned git
hook that bootstraps warden on a fresh clone — is SHA-256-verified against the
release's checksums.txt before it is made executable, and fails closed on any
mismatch; the cached binary is re-verified on every run. See
SECURITY.md for
the integrity model and the signature-verification follow-up.
cd your-repo
warden import --write # reads your Makefile / package.json / lefthook / CI into .warden.yaml
warden init # installs hooks + records the adoption pointwarden init alone also works — it auto-detects the language (Go, Rust, JS/TS,
Python) and pre-fills sensible lint/test commands.
Adopting a strict linter on a repo with existing debt, or running warden alongside Copilot review and automation PRs? See the adoption guide for gating the change (not the history) and the CI/bot settings that keep automated PRs from stalling.
cd your-repo
warden init # installs pre-commit + pre-push hooks, writes .warden.yaml,
# records an adoption point at HEAD
warden policy explain # print the resolved effective policy for a hypothetical pushFrom then on git commit / git push are gated. Warden's own push runs with
--no-verify so it never re-triggers the hook and recurses.
- pre-commit (fast, local): seeds a worktree from
HEAD+ staged changes, runs the fast step subset (default:lint), and re-applies any auto-fixes to your working tree. Passes → the commit proceeds. - pre-push (full pipeline): seeds a worktree from the branch tip, runs the
resolved pipeline (
intent → rebase → review → test → document → lint), pauses at an approval gate when a rule requires it, then fast-forwards your local branch to whatever the pipeline produced and writes a hash-chained provenance note underrefs/notes/wardenfor each validated commit. If the branch moved mid-run the fast-forward is aborted, never forced.
A passing push exits 0 and prints no error. When the pipeline changed
nothing, Warden stands aside and lets git perform and report the push itself, so
git push means exactly what it always did and its exit code answers "did it
land?" on its own.
Warden performs the push itself only when git cannot do it unaided: a step
rewrote the commit (an auto-fix, an amending agent step), the push needs a
policy-decided force (push.force: lease), or a PR is to be opened. Git resolves
the refs it will push before calling the hook and its push protocol is a
compare-and-swap, so its now-stale attempt would be rejected — Warden pushes,
then fails the hook on purpose to pre-empt it. Only on that path does git print
error: failed to push some refs, and Warden tells you first:
warden: pushed 9d90f7f3657c to origin/feature; local branch fast-forwarded
warden: git will now print 'error: failed to push some refs' — that's expected, not a failure…
Every validation note is signed with a per-machine ed25519 key (generated on
first run, kept under your user config dir — the private key never leaves the
machine). The signer's public key is bound into its own signature, so the note
proves not just that the evidence chain is intact but that a specific key
produced it. warden verify reports the signer; pass --key to require one:
warden key show # prints the fingerprint to pin
warden verify --key <fingerprint> # exit 0 only if signed by a trusted keyIn CI this turns provenance-skip from "a warden ran here" into "a warden I
trust ran here" — pass key: to the bundled warden-verify action. Notes
stay verifiable (chain + signature) without pinning; --key just adds the trust
gate.
Rather than pass fingerprints on every call, commit a trusted-signer roster
to .warden.yaml — a bare warden verify / --range then requires a trusted
signer automatically, and it inherits through extends: so an org names its
signers once:
# .warden.yaml
trusted_keys:
- 3a76a2b850d0e957 # add yours with `warden key show`; inspect with `warden key list`warden verify checks one commit (the provenance-skip primitive). To gate
a whole branch or PR — fail if any commit lacks trustworthy provenance — use
--range:
# fail unless every commit origin/main..HEAD is warden-validated
warden verify --range origin/main..HEAD
# escalate: each must be signed, and by a key in the trusted set
warden verify --range origin/main..HEAD --require-signed --key <fp1>,<fp2>
warden verify --range origin/main..HEAD --json # per-commit verdicts for CIIt exits non-zero with a per-commit reason — missing (no note),
broken-chain (a note that doesn't attest the commit — tampered or
transplanted), unsigned, or untrusted. Unlike warden doctor, which flags
only missing notes since adoption, --range also fails a tampered or
untrusted note, over an arbitrary BASE..HEAD. Merge commits are skipped by
default (--skip-merges); their parents are gated individually.
To turn this into a required check that blocks un-gated PRs from merging,
use the bundled warden-gate action — it runs verify --range on the PR head
(gating the merge before a squash rewrites history). See
CI provenance gate for the workflow and a
self-hosted pre-receive recipe, and ADR-0002
for the design.
Each note also carries a small SBOM: a SHA-256 digest of every dependency
lockfile present at validation (go.sum, package-lock.json, Cargo.lock, …).
Because it's part of the signed, hash-chained record, a validated commit ships a
tamper-evident, signed fingerprint of exactly which dependency sets it had —
shown by warden why.
extends: ../.warden.base.yaml # optional — inherit an org base config; this file overrides it
agent: auto
hooks: { pre_commit: true, pre_push: true }
commands:
lint: "golangci-lint run ./..."
test: "go test -race ./..."
# Agent steps (intent/review/document) run the command configured for the
# resolved agent, expanding {prompt}/{step}/{repo}. claude and codex work out of
# the box via bundled presets — you only need agent_commands to override those
# or add another agent. No command (and no preset) → advisory skip; Warden never
# guesses an agent's CLI.
agent_commands:
opencode: "opencode run {prompt}" # example: any other agent
steps:
pre_commit: [lint]
pre_push: [intent, rebase, review, test, document, lint, credentials] # credentials: refuse a push carrying a secret (see below)
parallel: true # default — run independent checks concurrently (see below)
writes: [codegen] # steps whose tree writes must be KEPT — run as sequential barriers (not isolated/discarded)
symlink_deps: false # default false = hardlink-copy node_modules into the worktree (works with Turbopack); true = fast symlink
timeouts: { test: "5m", review: "2m" } # kill + fail a step that hangs longer than this ("0" = no limit; a malformed value is rejected at load, never silently unlimited)
notify: true # default — desktop notification after a slow interactive pre-push (a failed/blocked push always notifies)
notify_after: 10s # default — a *passing* run only notifies once it ran at least this long (fast green gates stay silent); must be a valid Go duration or the config is rejected at load
cache: { test: ["**/*.go", "go.mod", "go.sum"] } # skip a step when its declared inputs are unchanged
risk: { diff_lines_high: 400, files_touched_high: 15 }
security_scan: { mode: delta } # default — the security-scan step fails only on findings THIS change introduced (see below)
pr: { enabled: true, comment: true } # open/update a PR on a passing push, post a gate-result comment
push: { force: lease } # default — a rebased branch is pushed with --force-with-lease pinned to the remote-tracking ref; `never` refuses instead (see below)
rules:
- match: { branch: main }
then: { require_approval: true, auto_fix: { test: 1 } }
- match: { paths: ["security/**", "auth/**"] }
then:
agent: { review: codex }
steps: { pre_push: { insert_after: lint, add: [security-scan] } }
- match: { risk: high }
then: { require_approval: true, agent: { review: claude } }All matching rules stack: per field the most specific wins (ties broken by
declaration order); step add/skip are unioned. warden policy explain
prints the result — the intended mitigation for a rule that misconfigures the
gate — including a schedule: line that shows exactly which steps run at once.
By default Warden runs independent steps concurrently, so the gate is as slow as the slowest step, not the sum of all of them:
schedule: intent → rebase → [review ∥ document ∥ test ∥ lint]
Every concurrent step runs in its own ephemeral worktree cloned from the
run's worktree, so steps can't race each other — even a coding-agent step
(review/document/intent) that edits files runs isolated, and its writes are
discarded when the batch finishes (only its findings are kept).
A step is instead a sequential barrier — it runs alone, in order, in the
shared worktree with its writes preserved — when its changes must be kept:
rebase (rewrites history), any step given an auto_fix budget (its fixes are
folded back into the tree), or a step you list under writes:. So to have a step
persist tracked-file changes — a codegen command, or a document agent that must
keep its docs — give it an auto_fix budget or add it to writes:. Set
parallel: false to force the classic one-step-at-a-time pipeline.
On an interactive terminal the pre-push run shows a live TUI: a spinner and a counting-up timer per step, a tail of each running step's output as it streams, and the approval gate answered inline.
Declare a step's input globs under cache: and warden skips it when every
matched file is byte-identical to the step's last passing run — so an unchanged
test doesn't re-run on a docs-only push. The cache lives in .git (per-clone,
never committed); the key also covers the step's command, so changing what the
step runs busts it. Only non-mutating steps are cacheable, and correctness rests
on declaring all of a step's inputs (same contract as bazel/turbo). A step's
first cache line appears as test (cached — inputs unchanged).
| Command | Description |
|---|---|
warden init [--hooks=pre-commit,pre-push] |
initialize, install hooks, record adoption point |
warden hooks enable|disable <hook> |
change hook selection |
warden run <pre-commit|pre-push> |
run the gate (invoked by the hook shims) |
warden policy explain [--hook h] [--branch b] [--paths glob,...] [--chart] |
print resolved policy (or an XState statechart) |
warden steps list |
list built-in + custom steps by hook |
warden import [--write] |
generate .warden.yaml from an existing Makefile / package.json / lefthook / CI |
warden status |
show gate state: armed hooks, adoption point, resolved steps |
warden doctor [--branch b] |
audit which commits since adoption carry a validation note |
warden audit [--branch b] [--format text|json|md] |
export a commit-provenance report (compliance) |
warden verify [--commit c] [--key fp] [--quiet] |
exit 0 if a commit is warden-validated — the CI provenance-skip primitive |
warden verify --range base..head [--require-signed] [--key fp] [--json] |
gate a whole range — exit non-zero if any commit lacks trusted provenance |
warden attest [--commit c] |
export a commit's provenance as an in-toto statement (sigstore/GUAC interop) |
warden reattest [--commit c] [--push] |
re-attest a squash-merge commit from the tree-identical validated commit |
warden reattest --all [--branch b] [--push] |
sweep a branch: re-attest every recoverable squash-merge gap since adoption |
warden key show |
print this machine's provenance signing key + fingerprint |
warden why [commit] |
explain what the gate did for a commit — matched rules, steps, signer — from its note |
warden recipes [name] |
list / print paste-able check recipes (gitleaks, semgrep, trivy, coverage-delta, …) |
warden watch |
re-run the fast checks on save — a continuous dev feedback loop |
warden attach |
watch a running gate live from another terminal (Unix socket) |
warden ci [--branch b] [--wait] |
report (or poll) CI status for the branch's PR |
warden axi <verb> |
flags-only agent surface, TOON output |
warden mcp serve |
MCP server over stdio |
The axi and mcp surfaces are non-interactive: they auto-approve gate
findings because there is no human at a prompt. That is fine for the read-only
operations (policy_explain / policy-explain, steps_list / steps), but
run_trigger (and warden axi run-trigger) executes the repository's
.warden.yaml commands as shell. Pointing an MCP-enabled agent at an
untrusted cloned repo and letting it call run_trigger would therefore be
arbitrary code execution from that repo's config, with the human-approval step a
normal interactive warden run keeps.
So run_trigger refuses by default on these surfaces and runs only when the
operator has explicitly trusted the repo:
- MCP (
warden mcp serve): setWARDEN_MCP_ALLOW_RUN=1in the server's environment. An MCP client cannot pass flags, so the env var is the only knob. - axi (
warden axi run-trigger): pass--trust, or setWARDEN_MCP_ALLOW_RUN=1.
Grant trust only for repositories whose .warden.yaml you have reviewed. The
normal interactive warden flow is unaffected — it still prompts a human.
Two ways, easy first.
Give a step a name and a command. Any step name with a commands.<name> entry
runs that command in the worktree; a non-zero exit fails the gate. This is the
common case — a custom check is just a command you already run.
commands:
security-scan: "nox scan . -severity-threshold high"
steps:
pre_push: [rebase, lint, security-scan, test]security-scan is the one command step Warden interprets rather than just runs.
When its command is a nox scan, Warden reads the
scan's findings.json and gates on what your change introduced, not on the
tree's total state:
security_scan:
mode: delta # default. total = fail on any unwaived finding, whoever added it
base: "" # default: merge-base with the branch's upstream (falls back to origin/HEAD)
version_check: true # default: refuse to scan when the local scanner isn't the version CI pins
pin_file: "" # default: search .github/workflows/*.yml for the pinWhy delta is the default. Gating on the tree's absolute state means an
unrelated one-line change inherits the repo's entire historical backlog as a
precondition. Measured across a fleet rollout, five repos had a finished,
unrelated config commit blocked by 7 / 16 / 21 / 44 / 71 pre-existing findings —
and the gate was red in 5 of 7 repos sampled while commits kept landing, i.e. it
was being routed around with --no-verify. A gate that is routinely bypassed
protects nothing and removes the signal that it ever ran. Delta gating keeps
the property that matters (you cannot add a vulnerability) and drops the wall:
pre-existing findings are reported as a counted warning. Set mode: total in a
repo that has genuinely reached zero and wants to stay there.
Scanner version drift. Scanners renumber their rule IDs between releases, so
the same hit gets a different fingerprint — and every entry in the committed
baseline stops matching at once. One repo pinned NOX_VERSION: 1.3.0 in CI while
developers ran 1.15.0: none of its 729 baseline entries matched anything CI
scanned, CI reported 240 phantom criticals, and every release failed for a month
before anyone noticed (the job only ran on tags). Warden now refuses to scan when
the scanner on PATH is not the version the repo's workflows pin, naming both
versions and the file that pins them, so the mismatch surfaces at pre-push where
it is cheap. The pin is read from the workflow rather than restated in
.warden.yaml — a second copy is a second thing to forget. Warden also reports a
baseline that matches zero current findings as drift rather than as hundreds
of new criticals. Rule of thumb: bump the pin and regenerate the baseline in
the same commit.
Anything Warden cannot interpret — make audit, npm audit, a nox command that
directs its own -output/-format — keeps the plain behavior: run it, fail on a
non-zero exit. The same is true whenever the report cannot be read or the base
commit cannot be scanned: the step degrades toward failing, never toward passing.
When a step needs to return per-file findings, request approval, or react to
earlier steps' findings, write a small program that speaks the JSON wire
protocol over stdin/stdout using the stepsdk package:
package main
import "go.klarlabs.de/warden/stepsdk"
func main() {
stepsdk.Run(func(in stepsdk.Input) stepsdk.Output {
// inspect in.RepoPath (the worktree), in.DiffSummary, in.PriorFindings...
return stepsdk.Pass()
})
}Build it as warden-step-<name> on PATH and reference <name> in the step
list. Either way, custom steps run as isolated subprocesses — no repo-authored
code is loaded into the daemon.
The security-scan step refuses to run when the scanner on your PATH differs
from the version CI pins — a scanner that renumbers rule IDs between releases
invalidates every baseline fingerprint at once, so the whole triaged corpus
reads as net-new.
It finds the pin by reading the workflow that already declares it. If your fleet pins the scanner once, centrally, in a shared reusable workflow, point at it across the repository boundary:
security_scan:
pin_file: my-org/.github/.github/workflows/go-ci.yml@mainThis names where the pin lives, never what it is — there is still exactly one
copy of the version number, in the workflow that already defines it. Both shapes
are understood: a scalar (NOX_VERSION: "1.16.1" in env: or with:) and a
reusable workflow's own input default, which is how the defining repo states
it.
Resolution is cached for an hour and bounded by a short timeout, and any
failure — offline, moved, renamed, no pin in the file — degrades to "no pin
found" rather than blocking a push. warden status reports when the check is
inert, so a silent check is never mistaken for a passing one.
The rebase step targets the branch's integration base — the ref it is
meant to merge into — resolved in order:
pr.basewhen the repo configures one (origin/<base>),- otherwise the remote's default head (
origin/HEAD→ e.g.origin/main), - otherwise an explicitly-set
@{upstream}, but only when it points somewhere other than this branch's own remote ref.
It deliberately never rebases onto origin/<this-branch>. That was the original
behaviour and it is correct only until the branch is rewritten: after a local
rebase onto an updated main — the standard way to satisfy "head branch is not
up to date with the base branch" — origin/<branch> still holds the commit you
just replaced, so origin/<branch>..HEAD contains main's commits. The step
would then replay main onto the superseded tip and fail on main's own conflicts,
refusing a push that was never wrong.
When no integration base exists (a brand-new branch, or a repo whose only branch is the one being pushed) the step passes and says so, rather than failing.
Some checkers guard a shared resource with a mutex and refuse to start while
another copy of themselves is running — golangci-lint ("parallel
golangci-lint is running"), cargo's build lock, and friends. That refusal is
not a verdict on your tree: the check never ran. It is also easy to trigger
by accident — run the linter in one terminal, commit from another.
Warden waits it out rather than reporting a lint error that doesn't exist. On a recognized contention message the step retries (up to a minute), saying so once:
warden: another process holds lint's lock, waiting…
If the lock is still held when that budget runs out, the gate still fails —
"I could not check" is not "the tree is clean" — but it says which, and exits
75 instead of 1 (see Exit codes):
warden: step lint could not run: another process holds its lock
Only a narrow set of known contention messages is retried, so a genuine failure fails immediately and keeps the tool's own output.
To stop it recurring with golangci-lint, add --allow-parallel-runners to
your lint command. That lock exists to keep concurrent runs from corrupting a
shared cache, and warden already points every run at its own
GOLANGCI_LINT_CACHE — so the thing the lock protects is already protected, and
two repos need never queue behind each other again:
commands:
lint: "golangci-lint run --allow-parallel-runners ./..."Warden says so in the failure message when it sees you haven't.
A validation worktree is a git worktree, so it starts with tracked files only. When a step's executable isn't there you get the shell's verdict, not the tool's:
> astro build
sh: astro: command not found
That is an environment failure, not a build failure, and warden reports it as
one — with the install command derived from the lockfile actually present, and
exit code 78:
warden: step js-build could not run: its toolchain or dependencies are not installed
[high] js-build could not run: astro is not installed in the validation worktree.
This is an environment problem, not a problem with your change.
Run: pnpm install --frozen-lockfile (in web)
The gate still fails: an unbuilt tree is not a validated tree. (Warden links or
copies gitignored node_modules from your live checkout into the worktree — see
symlink_deps / materialize_deps — so this usually means the deps aren't
installed in your checkout either.)
If installing needs auth for a private registry package, export the token as an environment variable:
export NODE_AUTH_TOKEN="$(gh auth token)" # in your shell / .envrc, not in the repoWarden passes your environment through to steps unchanged, and a tracked
.npmrc referring to ${NODE_AUTH_TOKEN} resolves against it:
@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
Do not reach for the command every guide suggests:
# DON'T — this writes a LIVE TOKEN into .npmrc
npm config set //npm.pkg.github.com/:_authToken "$(gh auth token)".npmrc is a tracked file in most repos, holding exactly the placeholder
above. npm config set overwrites it in place with the real credential, and the
next git add stages a secret. The whole point of a gate is that its happy path
can't end in a leak — so warden's built-in credentials step refuses the push if
it happens anyway.
Runs by default at pre-push. It reads the files your change touched and refuses the push if any of them carries something shaped like a live credential — GitHub/npm/AWS/Slack/Stripe/OpenAI/Anthropic/Google tokens and PEM private keys:
warden: step credentials failed
[high] .npmrc:2 looks like a live GitHub token (first 7 chars…). A tracked file must not
carry a credential — move it to an environment variable …
Matches are redacted in the output, and lines that defer to a variable
(${NODE_AUTH_TOKEN}, {{ secrets.X }}, $(vault read …)) or that hold an
obvious dummy (AWS's documented AKIA…EXAMPLE key) are not findings — a check that cries
wolf gets deleted, and then it catches nothing.
It is deliberately shallow: prefix-tagged token formats only, changed files
only, no entropy heuristics and no history. For real coverage add
warden recipes gitleaks. To turn it off, leave credentials out of
steps.pre_push.
warden run distinguishes "your change is wrong" from "this machine wasn't
ready", so a retry wrapper can tell them apart without parsing prose:
| Code | Meaning | Retry? |
|---|---|---|
0 |
passed (pre-commit, or a pre-push where git completed the push) | — |
1 |
the gate reached a verdict about your change — or a pre-push passed that warden had to push (see below) | no |
2 |
usage error | no |
75 |
a step couldn't run: another process holds its lock (EX_TEMPFAIL) |
yes, later |
78 |
a step couldn't run: its toolchain/deps aren't installed (EX_CONFIG) |
no — run the remediation |
A passing pre-push usually exits 0. It exits 1 in the one case where warden
performs the push itself — after a step rewrote the branch, or when a force is
needed — because git's own now-stale push must then be stopped from racing it.
So 1 stays ambiguous, which is exactly why the environmental cases get their
own codes.
Warden performs the push itself — it validates, then pushes, then fails the
hook so git's own (now redundant) push can't race it. A consequence: git hands
the pre-push hook no signal that you typed --force, so warden has to decide
for itself how to push a branch whose history you rewrote.
It detects the rewrite (the remote tip is no longer an ancestor of yours) and,
by default, pushes with --force-with-lease pinned to your remote-tracking
ref — the value you last fetched. A commit someone else pushed since then
invalidates the lease and the push is refused, so a rewrite can only ever
discard history you have actually seen.
push:
force: lease # default — rewrite with a lease when the branch was rebased
# force: never # refuse instead; the push fails as git wouldAn ordinary fast-forward never forces, whatever the setting — the flag is reserved for the case that needs it.
Why lease is the default. Because warden owns the push, refusing to
rewrite doesn't leave you with git's usual "use --force" nudge; it leaves you
with git push --no-verify, which skips the gate entirely and writes no
provenance. A default that pushes people toward bypassing the gate is worse
for the thing warden protects than one that rewrites a branch you already
rewrote locally. Set force: never if your repo would rather the push fail.
git ... --no-verify bypasses any hook by design; Warden does not fight that,
but makes it visible after the fact. Each validated commit gets a git note
carrying its axi-go evidence chain. warden doctor walks commits since the
adoption point and flags any without a matching note — so on a shared branch
every contributor can see which commits were actually validated, with no
central server. Note-push is best-effort: a failed note never blocks the push.
go build ./...
go test ./...Architecture (hexagonal): internal/domain (policy model), internal/policy
(rule resolution), internal/application (the pipeline Runner + ports),
internal/infrastructure/{git,kernel,steps,hooks,explain} (adapters),
internal/service (composition root), internal/cli + internal/mcp
(delivery), stepsdk (public custom-step SDK).
Contributions welcome — see CONTRIBUTING.md for the dev setup
and the make ci pipeline every change must pass. By participating you agree to
the Code of Conduct. Found a security issue? See
SECURITY.md — please don't open a public issue. Release history
is in the CHANGELOG.
MIT © Felix Geelhaar