Getting started#
This guide takes you from an empty deployment to a scheduled plumb run that
calls a maintenance agent when a check fails. For what each piece does, read
Concepts. For every key and option, read
Reference.
Requirements#
- Node.js 22 or newer (the package's
enginesfield). bashandgiton the machine that runs plumb. Plumb sources its config with bash, runs your checks with bash, and keeps the agent's clone with git.- A headless agent command, such as
claude -porcodex exec, that accepts a prompt as its last argument. The prompt tells the agent to open pull requests withgh pr createand to file issues, so the agent's environment needs access to your repository on GitHub.
Install#
Install plumb as a dependency of the deployment, so the version is pinned there:
npm install @kontourai/plumb
The package provides the plumb command. Without a local install,
npx @kontourai/plumb run resolves it from the registry instead.
Write a checks command#
A checks command is any shell command that prints evidence and exits non-zero when a hard check fails. Plumb does not care what language it is written in. A minimal one:
#!/bin/bash
set -uo pipefail
FAILED=0
if curl --fail --silent --show-error http://localhost:3000/api/health; then
echo "OK app-health"
else
echo "FAIL app-health"
FAILED=1
fi
exit "$FAILED"
examples/checks-wrapper.sh shows a fuller
wrapper with hard checks, a soft warning that never fails the run, and a
separate pure checker. See Examples for the rest.
Create the config#
Copy plumb.config.example next to the deployment,
not inside the plumb package, and fill in the five required keys:
REPO_URL="git@github.com:you/your-app.git"
CHECKS_CMD="node scripts/checks.mjs"
AGENT_WORKDIR="$HOME/agent-work/your-app"
AGENT_CMD="claude -p --dangerously-skip-permissions"
LOG_FILE="$HOME/.local/state/plumb/checks.log"
AGENT_WORKDIR must be a directory reserved for the agent. Plumb clones
REPO_URL there, and before each escalation that passes the guards it fetches
origin and, if the fetch succeeds, runs git reset --hard origin/main. Never
point it at the deployment checkout.
The file is bash: plumb loads it by sourcing it, so comments, quoting, and
$HOME expansion work as they do in a shell script. Point PLUMB_CONFIG at it:
export PLUMB_CONFIG="$HOME/infra/my-app/plumb.config"
Run the checks once#
plumb run
Plumb appends a header line and the checks command's output to LOG_FILE:
=== 2026-07-18T07:15:00.000Z plumb checks
OK app-health
If the checks command exits non-zero, plumb escalates: it runs the agent in
AGENT_WORKDIR and appends the agent's output to the escalations log beside
LOG_FILE (checks-escalations.log for checks.log). plumb run still exits
zero, so a failing check never makes the scheduled job itself fail. It exits 1
only when it cannot start, for example when a required config key is missing.
Schedule it#
Run plumb run from cron, or use the systemd units in
systemd/.
plumb-checks.service is a oneshot service
that sets PLUMB_CONFIG and runs the deployment's pinned
node_modules/.bin/plumb run, and
plumb-checks.timer triggers it daily at
07:15. Replace APP in the service with your deployment directory.
Tag deploys#
At the end of a deployment, pass the deployed commit:
plumb run --context deploy="$DEPLOY_SHA"
If that run fails, the agent is told which commit just landed. When plumb has recorded a different earlier deploy SHA and both commits exist in the agent's clone, it also gets the git range and diffstat between them. The first deploy run has no earlier SHA, so it gets neither. See Run context and deploy provenance.
Escalate from your own jobs#
Any job can hand its own failure log to the same guarded agent:
plumb escalate database-refresh /var/log/my-app/database-refresh.log
Give each failure mode its own context name so they do not share a cooldown.
examples/job-with-escalation.sh shows a
cron job with two independently guarded steps.