Skip to content

sdks

CLI

The deadpost binary wraps a command and reports how it went — the start, the finish, the duration and the exit code — without changing what the command does.

0 3 * * * deadpost run -m daily-backup -- /usr/local/bin/backup.sh

That line behaves exactly like the one it replaced: same exit status, same output, same cron mail on failure.

It’s a single static binary with no runtime and no dependencies, which is the point — a Python-shipped wrapper needs Python on every monitored box, and distroless containers, Alpine cron images and FROM scratch deploys don’t have one.

# Homebrew
brew install deadpost/tap/deadpost
# script (Linux, macOS) — reads the script before running it
curl -fsSL https://deadpost.dev/install.sh | sh
# Go
go install github.com/deadpost/deadpost-cli@latest

Builds are published for Linux, macOS and Windows on amd64 and arm64, as .deb and .rpm packages, and through Homebrew and Scoop.

Command What it does
deadpost run [flags] -- <cmd> run a command, report /start then /end or /fail
deadpost ping “I ran and succeeded” — the one-shot heartbeat
deadpost start / end / fail bracket a section of a shell script by hand
deadpost flush replay check-ins buffered while the API was unreachable
deadpost doctor validate the config and round-trip the API
deadpost version version, commit, Go version, platform

deadpost <command> --help prints that command’s flags.

deadpost run sits in front of a production job, so it is built to be indistinguishable from not being there:

  • The child’s exit code is returned verbatim. deadpost run -- false exits 1, so &&, set -e and cron’s mail-on-failure keep working.
  • Streams pass through. stdout and stderr are teed, never swallowed — cron’s mail still carries the job’s own output.
  • Signals forward to the child, and the run is reported failed once the child is actually gone.
  • The wrapper never adds a failure mode. If the API is unreachable, or the token is missing, or the monitor name is a typo, the child still runs and its exit code is still returned.

There is one deliberate exception. --timeout exits 124, following timeout(1), rather than passing the child’s status through — because we killed the job, and a child that traps SIGTERM and exits 0 on its way out would otherwise tell && it succeeded while we report it failed.

Other codes the wrapper produces itself: 2 for a usage error, 127 for a command that can’t be executed, and 128+N for a child killed by signal N.

# Ship metrics with the outcome. Numbers stay numbers.
deadpost run --tag rows=1234 --tag env=prod -- ./nightly-etl.sh
# Kill a job that hangs, and hear about it.
deadpost run --timeout 30m -- ./import.sh
# Attach the failing job's output to the alert.
deadpost run --capture -- ./flaky.sh
# Already a pipeline? Just the heartbeat.
pg_dump prod | gzip > /backups/db.gz && deadpost ping

To bracket part of an existing script rather than wrap the whole thing:

deadpost start
./stage-one.sh || { deadpost fail --error "stage one"; exit 1; }
./stage-two.sh
deadpost end

--capture keeps the last 4 KiB of each stream and ships it on failure only, so a successful nightly job doesn’t post its log every night.

Flags win over the environment, which wins over the defaults.

Variable Default Notes
DEADPOST_TOKEN the default monitor’s ping token
DEADPOST_TOKEN_<NAME> registers monitor <name>; address it with -m <name>
DEADPOST_PING_URL https://ping.deadpost.dev
DEADPOST_DISABLED false kill switch — every check-in becomes a no-op
DEADPOST_TIMEOUT 5s per HTTP attempt
DEADPOST_DEADLINE 10s total across retries
DEADPOST_RETRIES 3 attempts, not retries-after-first
DEADPOST_SPOOL false buffer failed check-ins on disk
DEADPOST_SPOOL_DIR $XDG_STATE_HOME/deadpost/spool see below for Windows
DEADPOST_MAX_REPLAY_AGE 15m buffered check-ins older than this are dropped unsent
DEADPOST_LOG_LEVEL warning debug, info, warning, error, silent
NO_COLOR set to anything to turn off coloured output

On Windows the spool defaults to %LOCALAPPDATA%\deadpost\spool.

DEADPOST_TOKEN_<NAME> registers a named monitor, and -m <name> picks it:

export DEADPOST_TOKEN_DAILY_BACKUP=00000000-0000-7000-8000-000000000001
deadpost run -m daily-backup -- /usr/local/bin/backup.sh

Name matching folds case and treats _ and - alike, because environment variable names can’t contain hyphens and hyphens are what people name monitors with.

DEADPOST_DISABLED=1 turns every check-in into a no-op. Without it, running a monitored job on your laptop checks the production monitor in — which is worse than it sounds, because it means a job that failed in production looks healthy.

Colour is on only when the stream is a terminal, so cron mail, a redirected log and deadpost doctor | grep ✗ all get plain text without being configured. --color always|never overrides the detection — always is what you want when piping into something that does render ANSI, like less -R.