Event offer · 100 free credits
AlertINT reads each Zabbix problem and posts the likely cause to Slack, ntfy or your AI agent.

To try AlertINT on your own alerts, while the event pool lasts.
Just your email.
It arrives by email.
Pass the key in the command.
curl -fsSL https://alertint.com/zabbix/install.sh | \ALERTINT_TRIAL_KEY=YOUR_KEY_FROM_EMAIL shSample with invented hosts and cause. Grouping across hosts needs a shared trigger tag (here service=checkout) and one config line.
1 incident: checkout · 6 problems grouped
db-01 ran out of disk on /var/lib/postgresql.
Postgres stopped taking writes, so connections piled up and the app and web servers slowed down.
Symptoms, not causes
Evidence
Disk free fell from 31% to 0% in 40 minutes, after the 13:48 config change.
Administration, General, Modules: Scan directory, then enable AlertINT. The script installs the module when Zabbix runs on the same machine. Otherwise copy it to your Zabbix frontend.
AlertINT, Settings. The script prints the agent address and two tokens to paste.
Connect sets up the media type, a service user that cannot log in, and the trigger action. Send a test problem: about a minute later AlertINT shows its finding.
$ curl -fsSL https://alertint.com/zabbix/install.sh | ALERTINT_TRIAL_KEY=YOUR_KEY_FROM_EMAIL sh AlertINT · installer for Zabbix ✔ Detected linux / amd64 (Ubuntu 26.04 LTS), systemd ✔ Release v0.16.0 ✔ Downloaded alertint_0.16.0_linux_amd64.tar.gz (11 MB, SHA-256 checked) ✔ Installed /usr/local/bin/alertint ✔ Configured /etc/alertint/config.yaml (key stored 0640, root + alertint only) ✔ Running alertint service, starts on boot (webhook :9911, MCP :9912) ✔ Module AlertINT 0.1.0 in /usr/share/zabbix/ui/modules/alertint Done in 4s. AlertINT is running. Finish in Zabbix, signed in as a Super admin: 1. Administration > General > Modules: press Scan directory, enable AlertINT. 2. AlertINT > Settings, section Agent: enter these two, then Save. Agent MCP address http://127.0.0.1:9912/mcp MCP token •••• (64 characters, hidden here) 3. Section Connect Zabbix: enter this one, then Connect. Webhook token •••• (64 characters, hidden here) 4. Press Send test problem. About a minute later AlertINT shows its finding. The tokens are also in /etc/alertint/alertint.env (root only).Real output from a run on Ubuntu 26.04, on the Zabbix frontend host. Tokens hidden.
| Parameter | Value |
|---|---|
| url | http://<alertint-host>:9911/webhook/zabbix |
| token | <your ALERTINT_ZABBIX_WEBHOOK_TOKEN> |
| event_id | {EVENT.ID} |
| status | {EVENT.STATUS} |
| severity | {EVENT.SEVERITY} |
| host | {HOST.HOST} |
| trigger_name | {TRIGGER.NAME} |
| item_key | {ITEM.KEY} |
| item_value | {ITEM.VALUE} |
| tags | {EVENT.TAGSJSON} |
Ten of the sixteen media type parameters. Connect imports the media type and fills them in.
With a read-only Zabbix token, AlertINT can also read item history, other open problems and host inventory while it investigates. In AlertINT, Settings, section 4, press Create read access. It makes the token and prints one command for the agent host:
$ curl -fsSL https://alertint.com/zabbix/install.sh | \ZABBIX_URL=https://zabbix.example.com \ZABBIX_API_TOKEN=TOKEN_FROM_SETTINGS shIt keeps the agent, its tokens and settings, and checks that the token can read Zabbix. Then press Check in Settings.
Without the module, create the token by hand, then add ZABBIX_URL and ZABBIX_API_TOKEN to the install command (first run or a later one):
With that role, even a leaked token cannot change anything in Zabbix.
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://zbx-monitor:9911/webhook/zabbix -d '{}'401 $ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://zbx-monitor:9911/webhook/zabbix \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"event_id":"1001","status":"PROBLEM","severity":"High","nseverity":"3","host":"db-01", "host_visible":"db-01","trigger_id":"42","trigger_name":"Disk space is low on /var", "item_key":"vfs.fs.size[/var,pfree]","item_value":"4 %","tags":[], "clock":"2026.11.02 10:00:00","recovery_clock":"","generator_url":""}'204Real requests: 401 without the token, 204 with it.
#!/bin/sh
# AlertINT for Zabbix installer.
#
# curl -fsSL https://alertint.com/zabbix/install.sh | ALERTINT_TRIAL_KEY=YOUR_KEY sh
#
# Installs and starts the AlertINT agent (systemd). On a Zabbix frontend host it
# also installs the AlertINT Zabbix module. Then it prints what to enter in
# Zabbix → AlertINT → Settings. It never changes your Zabbix configuration.
#
# Flags (pass after `sh -s --`): --uninstall --help
# Env: ALERTINT_TRIAL_KEY or KEY your LLM key (asked for if missing)
# ZABBIX_URL + ZABBIX_API_TOKEN optional read-only Zabbix API (also on a re-run)
# ALERTINT_VERSION pin an agent release, e.g. v0.16.0
# ALERTINT_MODULES_DIR Zabbix frontend modules folder (found automatically)
# ALERTINT_PUBLIC_HOST agent address to show, when it can't be guessed
# NO_COLOR
#
# Everything lives inside main() and runs on the last line, so a truncated
# download can never execute half a script.
REPO="alertint/alertint-agent"
MODULE_REPO="alertint/alertint-zabbix"
BIN_DIR="/usr/local/bin"
CONF_DIR="/etc/alertint"
DATA_DIR="/var/lib/alertint"
SVC_USER="alertint"
WEBHOOK_PORT=9911
MCP_PORT=9912
SELF_URL="https://alertint.com/zabbix/install.sh"
# ---------- terminal UX -------------------------------------------------------
setup_ui() {
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
B=$(printf '\033[1m'); D=$(printf '\033[2m'); R=$(printf '\033[0m')
RED=$(printf '\033[31m'); GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); CYN=$(printf '\033[36m')
TTY=1
else
B=""; D=""; R=""; RED=""; GRN=""; YEL=""; CYN=""; TTY=0
fi
}
banner() { printf '\n %sAlertINT%s · installer for Zabbix\n\n' "$B$CYN" "$R"; }
ok() { printf ' %s✔%s %s%-12s%s %s\n' "$GRN" "$R" "$B" "$1" "$R" "$2"; }
warn() { printf ' %s!%s %s%-12s%s %s\n' "$YEL" "$R" "$B" "$1" "$R" "$2"; }
info() { printf ' %s%s%s\n' "$D" "$1" "$R"; }
die() {
printf '\n %s✘ %s%s\n' "$RED$B" "$1" "$R" >&2
[ -n "${2:-}" ] && printf ' %s%s%s\n' "$D" "$2" "$R" >&2
printf '\n' >&2
exit 1
}
# run_step "label" cmd args... — spinner on a terminal, quiet otherwise.
# Output goes to $LOG; shown only on failure.
run_step() {
_label=$1; shift
if [ "$TTY" = 1 ]; then
# Spinner runs in the background; the step itself runs in THIS shell so
# the variables it sets (WORK, SIZE, ...) are kept.
(
_i=0
while :; do
case $((_i % 4)) in 0) _c='⠋';; 1) _c='⠙';; 2) _c='⠹';; *) _c='⠸';; esac
printf '\r %s%s%s %s' "$CYN" "$_c" "$R" "$_label"
_i=$((_i + 1)); sleep 0.1
done
) &
SPIN_PID=$!
"$@" >>"$LOG" 2>&1
_rc=$?
kill "$SPIN_PID" 2>/dev/null; wait "$SPIN_PID" 2>/dev/null; SPIN_PID=""
printf '\r\033[K'
return $_rc
else
"$@" >>"$LOG" 2>&1
fi
}
# ---------- helpers -----------------------------------------------------------
have() { command -v "$1" >/dev/null 2>&1; }
detect_platform() {
case "$(uname -s)" in
Linux) OS=linux ;;
Darwin) OS=darwin ;;
*) die "Unsupported OS: $(uname -s)" "AlertINT ships binaries for Linux and macOS." ;;
esac
case "$(uname -m)" in
x86_64|amd64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
*) die "Unsupported CPU: $(uname -m)" "AlertINT ships amd64 and arm64 binaries." ;;
esac
PRETTY="$OS / $ARCH"
if [ "$OS" = linux ] && [ -r /etc/os-release ]; then
_n=$(. /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-}")
[ -n "$_n" ] && PRETTY="$PRETTY ($_n)"
fi
HAS_SYSTEMD=0
if [ "$OS" = linux ] && [ -d /run/systemd/system ] && have systemctl; then HAS_SYSTEMD=1; fi
[ "$HAS_SYSTEMD" = 1 ] && PRETTY="$PRETTY, systemd"
}
setup_root() {
if [ "$(id -u)" = 0 ]; then SUDO=""; return; fi
have sudo || die "Root access needed" "Run as root, or install sudo."
SUDO="sudo"
if ! sudo -n true 2>/dev/null; then
info "Not root: using sudo for system changes (you may be asked for your password)."
sudo -v </dev/tty || die "sudo failed" "Run this installer as root or fix sudo access."
fi
}
fetch() { curl -fsSL --retry 3 --connect-timeout 10 "$@"; }
latest_tag() {
if [ -n "${ALERTINT_VERSION:-}" ]; then
TAG=$ALERTINT_VERSION
case $TAG in v*) ;; *) TAG="v$TAG" ;; esac
return
fi
# Follows the /releases/latest redirect: no GitHub API rate limit.
_u=$(curl -fsSL --retry 3 --connect-timeout 10 -o /dev/null -w '%{url_effective}' \
"https://github.com/$REPO/releases/latest") || return 1
TAG=${_u##*/}
case $TAG in v[0-9]*) ;; *) return 1 ;; esac
}
sha256_of() {
if have sha256sum; then sha256sum "$1" | cut -d' ' -f1
elif have shasum; then shasum -a 256 "$1" | cut -d' ' -f1
else return 1; fi
}
rand_token() {
if have openssl; then openssl rand -hex 32
else head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'; fi
}
get_key() {
KEY=${ALERTINT_TRIAL_KEY:-${KEY:-}}
if [ -z "$KEY" ] && [ -r /dev/tty ] && [ -t 1 ]; then
printf ' %sYour key%s (from the email; input hidden): ' "$B" "$R" >/dev/tty
stty -echo </dev/tty 2>/dev/null
read -r KEY </dev/tty
stty echo </dev/tty 2>/dev/null
printf '\n' >/dev/tty
fi
[ -n "$KEY" ] || return 1
case $KEY in sk-ant-*) ;; *) KEYWARN=1 ;; esac
}
env_get() { # env_get NAME FILE -> value (root-readable file)
$SUDO sed -n "s/^$1=//p" "$2" 2>/dev/null | head -n 1
}
# ZABBIX_URL as the agent wants it: the frontend root, no trailing slash and
# no /api_jsonrpc.php (the agent appends that itself).
normalize_zabbix_url() {
ZABBIX_URL=$(printf '%s' "$ZABBIX_URL" | sed -e 's#/*$##' -e 's#/api_jsonrpc\.php$##' -e 's#/*$##')
case $ZABBIX_URL in
http://?*|https://?*) ;;
*) return 1 ;;
esac
case $ZABBIX_URL in *[\ \"\'\\]*) return 1 ;; esac
}
# zabbix_api_patch URL <config.yaml >patched
# Turns on the read-only Zabbix API in an existing config.yaml:
# - the installer's commented-out api block -> a live block
# - a live api block -> base_url (and token env) set
# - a zabbix block without one -> a live block appended to it
# Exit 3, and the caller keeps the file, for any other shape (hand-edited
# indentation, flow style, no zabbix block, two api blocks).
zabbix_api_patch() {
awk -v url="$1" '
function emit() {
print " api:"; print " base_url: \"" url "\""; print " api_token_env: ZABBIX_API_TOKEN"; done = 1
}
function close_api() {
if (!gotbase) print " base_url: \"" url "\""
if (!gottok) print " api_token_env: ZABBIX_API_TOKEN"
inapi = 0; done = 1
}
function flush() { for (i = 1; i <= nb; i++) print buf[i]; nb = 0 }
{
if ($0 ~ /^zabbix:/) {
if ($0 !~ /^zabbix:[ \t]*(#.*)?$/ || sawz) bad = 1
sawz = 1; inz = 1; print; next
}
if (!inz) { print; next }
if ($0 ~ /^[ \t]*$/ || $0 ~ /^#/) { buf[++nb] = $0; next }
if ($0 ~ /^[^ \t#]/) {
if (inapi) close_api(); else if (!done) emit()
flush(); inz = 0; print; next
}
if (inapi && $0 !~ /^ /) close_api()
flush()
if (!child++ && $0 !~ /^ [^ ]/) bad = 1 # children must sit at 2 spaces
if ($0 ~ /^\t/ || $0 ~ /^ [^ ]/ || $0 ~ /^ [^ ]/) { bad = 1; print; next }
if ($0 ~ /^ # (Read-only Zabbix API|ZABBIX_URL and|ZABBIX_URL \+|api:| base_url:| api_token_env:)/) {
if ($0 ~ /^ # api:/ && !done) emit()
next
}
if ($0 ~ /^ api:/) {
if ($0 !~ /^ api:[ \t]*(#.*)?$/ || done) bad = 1
inapi = 1; gotbase = 0; gottok = 0; print; next
}
if (inapi) {
if ($0 ~ /^ base_url:/) { print " base_url: \"" url "\""; gotbase = 1; next }
if ($0 ~ /^ api_token_env:/) { print " api_token_env: ZABBIX_API_TOKEN"; gottok = 1; next }
if ($0 ~ /^ enabled:/) { print " enabled: true"; next }
}
print
}
END {
if (inz) { if (inapi) close_api(); else if (!done) emit() }
flush()
if (!sawz || bad) exit 3
}
'
}
# Asks the Zabbix API, as the token, how many hosts it can read: from this host,
# like the agent will. Sets ZBX_HOSTS, or ZBX_CHECK with the reason it failed.
# The token goes to curl on stdin (-K -), never on the command line.
check_zabbix_api() {
ZBX_HOSTS=""; ZBX_CHECK=""
_resp=$(printf 'header = "Authorization: Bearer %s"\n' "$ZABBIX_API_TOKEN" |
curl -sS --connect-timeout 5 --max-time 15 -K - -H 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"host.get","params":{"countOutput":true},"id":1}' \
"$ZABBIX_URL/api_jsonrpc.php" 2>&1) || {
ZBX_CHECK="could not reach $ZABBIX_URL/api_jsonrpc.php ($(printf '%s' "$_resp" | head -n 1))"
return 1
}
ZBX_HOSTS=$(printf '%s' "$_resp" | sed -n 's/.*"result":"\([0-9][0-9]*\)".*/\1/p')
[ -n "$ZBX_HOSTS" ] && return 0
ZBX_CHECK=$(printf '%s' "$_resp" | sed -n 's/.*"data":"\([^"]*\)".*/\1/p')
[ -n "$ZBX_CHECK" ] || ZBX_CHECK="no Zabbix API answer at $ZABBIX_URL/api_jsonrpc.php"
return 1
}
# The Zabbix frontend's modules folder on this host, if there is one:
# 7.2+ packages move the frontend to /usr/share/zabbix/ui.
find_modules_dir() {
if [ -n "${ALERTINT_MODULES_DIR:-}" ]; then MODULES_DIR=$ALERTINT_MODULES_DIR; return 0; fi
for _d in /usr/share/zabbix/ui /usr/share/zabbix /usr/share/zabbix-frontend-php; do
if [ -f "$_d/zabbix.php" ]; then MODULES_DIR="$_d/modules"; return 0; fi
done
return 1
}
# The address Zabbix should use for the agent: this host's first IP.
guess_host() {
if [ -n "${ALERTINT_PUBLIC_HOST:-}" ]; then printf '%s' "$ALERTINT_PUBLIC_HOST"; return; fi
_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$_ip" ] || _ip=$(hostname 2>/dev/null)
printf '%s' "${_ip:-<this-host>}"
}
# ---------- install steps -----------------------------------------------------
download_and_verify() {
WORK=$(mktemp -d "${TMPDIR:-/tmp}/alertint.XXXXXX") || return 1
ASSET="alertint_${TAG#v}_${OS}_${ARCH}.tar.gz"
BASE="https://github.com/$REPO/releases/download/$TAG"
fetch -o "$WORK/$ASSET" "$BASE/$ASSET" || return 1
fetch -o "$WORK/SHA256SUMS" "$BASE/SHA256SUMS" || return 1
WANT=$(grep " $ASSET\$" "$WORK/SHA256SUMS" | cut -d' ' -f1)
[ -n "$WANT" ] || { echo "no checksum entry for $ASSET"; return 1; }
GOT=$(sha256_of "$WORK/$ASSET") || { echo "no sha256sum/shasum available"; return 1; }
[ "$WANT" = "$GOT" ] || { echo "checksum mismatch: want $WANT got $GOT"; return 1; }
tar -xzf "$WORK/$ASSET" -C "$WORK" || return 1
[ -f "$WORK/alertint" ] || { echo "binary 'alertint' not found in archive"; return 1; }
SIZE=$(wc -c <"$WORK/$ASSET" | tr -d ' ')
}
install_binary() {
$SUDO mkdir -p "$BIN_DIR" &&
$SUDO install -m 0755 "$WORK/alertint" "$BIN_DIR/alertint"
}
ensure_user_dirs() {
if ! id "$SVC_USER" >/dev/null 2>&1; then
$SUDO useradd --system --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$SVC_USER" || return 1
fi
$SUDO mkdir -p "$CONF_DIR" "$DATA_DIR" &&
$SUDO chown "$SVC_USER:$SVC_USER" "$DATA_DIR" &&
$SUDO chmod 0750 "$DATA_DIR" "$CONF_DIR" &&
$SUDO chgrp "$SVC_USER" "$CONF_DIR"
}
write_config() {
ENVF="$CONF_DIR/alertint.env"
CONF="$CONF_DIR/config.yaml"
if $SUDO test -f "$ENVF"; then
WEBHOOK_TOKEN=$(env_get ALERTINT_ZABBIX_WEBHOOK_TOKEN "$ENVF")
MCP_TOKEN=$(env_get ALERTINT_MCP_TOKEN "$ENVF")
[ -n "${KEY:-}" ] || KEY=$(env_get ANTHROPIC_API_KEY "$ENVF")
[ -n "${ZABBIX_API_TOKEN:-}" ] || ZABBIX_API_TOKEN=$(env_get ZABBIX_API_TOKEN "$ENVF")
REUSED=1
fi
[ -n "${WEBHOOK_TOKEN:-}" ] || WEBHOOK_TOKEN=$(rand_token)
[ -n "${MCP_TOKEN:-}" ] || MCP_TOKEN=$(rand_token)
# Secrets go through stdin, never through command-line arguments (ps).
{
printf 'ANTHROPIC_API_KEY=%s\n' "$KEY"
printf 'ALERTINT_ZABBIX_WEBHOOK_TOKEN=%s\n' "$WEBHOOK_TOKEN"
printf 'ALERTINT_MCP_TOKEN=%s\n' "$MCP_TOKEN"
[ -n "${ZABBIX_API_TOKEN:-}" ] && printf 'ZABBIX_API_TOKEN=%s\n' "$ZABBIX_API_TOKEN"
} | $SUDO tee "$ENVF" >/dev/null || return 1
$SUDO chown root:"$SVC_USER" "$ENVF" && $SUDO chmod 0640 "$ENVF" || return 1
if ! $SUDO test -f "$CONF"; then
if [ -n "${ZABBIX_URL:-}" ] && [ -n "${ZABBIX_API_TOKEN:-}" ]; then
ZBX_API=" api:
base_url: \"$ZABBIX_URL\"
api_token_env: ZABBIX_API_TOKEN"
ZBX_API_STATE=added
else
ZBX_API=" # Read-only Zabbix API for richer findings: re-run the installer with
# ZABBIX_URL + ZABBIX_API_TOKEN set, and it turns this block on.
# api:
# base_url: https://zabbix.example.com
# api_token_env: ZABBIX_API_TOKEN"
fi
$SUDO tee "$CONF" >/dev/null <<EOF || return 1
# Written by the AlertINT installer for Zabbix. Safe to edit; re-running the
# installer keeps this file. Secrets live in alertint.env, not here.
# Every option: https://alertint.com/docs/getting-started/configuration
receivers:
address: "0.0.0.0:$WEBHOOK_PORT" # the Zabbix server posts problems here
alertmanager:
enabled: false
zabbix:
instance_id: zabbix-$(hostname 2>/dev/null | tr -c 'a-zA-Z0-9\n-' '-')
ingress:
enabled: true
webhook_token_env: ALERTINT_ZABBIX_WEBHOOK_TOKEN
$ZBX_API
storage:
sqlite_path: $DATA_DIR/alertint-agent.db
llm:
provider: anthropic
api_key_env: ANTHROPIC_API_KEY
notify:
stdout: true
mcp:
addr: "0.0.0.0:$MCP_PORT" # the Zabbix module (and your AI tools) read here
token_env: ALERTINT_MCP_TOKEN
EOF
$SUDO chown root:"$SVC_USER" "$CONF" && $SUDO chmod 0640 "$CONF"
elif [ -n "${ZABBIX_URL:-}" ] && [ -n "${ZABBIX_API_TOKEN:-}" ]; then
# Re-run with the Zabbix API: turn it on in the kept config.yaml.
# The config holds no secrets, so the patch is staged in $WORK.
$SUDO cat "$CONF" >"$WORK/config.in" || return 1
if zabbix_api_patch "$ZABBIX_URL" <"$WORK/config.in" >"$WORK/config.new"; then
if cmp -s "$WORK/config.in" "$WORK/config.new"; then
ZBX_API_STATE=unchanged
else
$SUDO cp -p "$CONF" "$CONF.bak" &&
$SUDO tee "$CONF" <"$WORK/config.new" >/dev/null || return 1
ZBX_API_STATE=updated
fi
else
ZBX_API_STATE=manual
fi
elif [ -n "${ZABBIX_API_TOKEN:-}" ] && ! $SUDO grep -q '^ api:' "$CONF"; then
ZBX_API_STATE=no_url
fi
if [ -n "${ZABBIX_URL:-}" ] && [ -z "${ZABBIX_API_TOKEN:-}" ]; then ZBX_API_STATE=no_token; fi
return 0
}
write_unit() {
$SUDO tee /etc/systemd/system/alertint.service >/dev/null <<EOF || return 1
[Unit]
Description=AlertINT agent
After=network-online.target
Wants=network-online.target
[Service]
User=$SVC_USER
Group=$SVC_USER
EnvironmentFile=$CONF_DIR/alertint.env
WorkingDirectory=$DATA_DIR
ExecStart=$BIN_DIR/alertint serve --config $CONF_DIR/config.yaml
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
ReadWritePaths=$DATA_DIR
[Install]
WantedBy=multi-user.target
EOF
$SUDO systemctl daemon-reload &&
$SUDO systemctl enable alertint >/dev/null 2>&1 &&
$SUDO systemctl restart alertint
}
wait_healthy() {
_n=0
while [ $_n -lt 30 ]; do
$SUDO systemctl is-active --quiet alertint || { sleep 1; _n=$((_n + 1)); continue; }
_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 2 "http://127.0.0.1:$WEBHOOK_PORT/" 2>/dev/null)
[ "$_code" != "000" ] && [ -n "$_code" ] && return 0
sleep 1; _n=$((_n + 1))
done
return 1
}
# The Zabbix module: latest release of alertint/alertint-zabbix, checksum-verified,
# unpacked to <modules>/alertint. Replaces an older copy there.
install_module() {
_url=${ALERTINT_MODULE_URL:-https://github.com/$MODULE_REPO/releases/latest/download/alertint-zabbix.tar.gz}
_sums=${_url%/*}/SHA256SUMS
mkdir -p "$WORK/module" || return 1
fetch -o "$WORK/alertint-zabbix.tar.gz" "$_url" || return 1
fetch -o "$WORK/module-SHA256SUMS" "$_sums" || return 1
_want=$(grep ' alertint-zabbix.tar.gz$' "$WORK/module-SHA256SUMS" | cut -d' ' -f1)
_got=$(sha256_of "$WORK/alertint-zabbix.tar.gz") || return 1
[ -n "$_want" ] && [ "$_want" = "$_got" ] || { echo "module checksum mismatch"; return 1; }
tar -xzf "$WORK/alertint-zabbix.tar.gz" -C "$WORK/module" || return 1
[ -f "$WORK/module/alertint/manifest.json" ] || { echo "no alertint/manifest.json in the module archive"; return 1; }
MODULE_VERSION=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$WORK/module/alertint/manifest.json" | head -n 1)
$SUDO mkdir -p "$MODULES_DIR" &&
$SUDO rm -rf "$MODULES_DIR/alertint" &&
$SUDO cp -R "$WORK/module/alertint" "$MODULES_DIR/alertint" &&
$SUDO chown -R root:root "$MODULES_DIR/alertint" &&
$SUDO chmod -R u=rwX,go=rX "$MODULES_DIR/alertint"
}
uninstall() {
setup_root
if [ "$OS" = linux ] && [ "$HAS_SYSTEMD" = 1 ]; then
$SUDO systemctl disable --now alertint >/dev/null 2>&1
$SUDO rm -f /etc/systemd/system/alertint.service && $SUDO systemctl daemon-reload
ok "Service" "stopped and removed"
fi
$SUDO rm -f "$BIN_DIR/alertint"
ok "Binary" "removed from $BIN_DIR"
if find_modules_dir && [ -d "$MODULES_DIR/alertint" ]; then
info "The Zabbix module stays in $MODULES_DIR/alertint. Disable it in"
info "Administration > General > Modules first, then delete that folder."
fi
info "Kept: $CONF_DIR (config + keys) and $DATA_DIR (incident history)."
info "Delete them yourself for a full wipe: sudo rm -rf $CONF_DIR $DATA_DIR"
printf '\n'
}
# The read-only Zabbix API: what changed, and whether the token works from here.
report_zabbix_api() {
case $ZBX_API_STATE in
added|updated|unchanged)
_what="on"; [ "$ZBX_API_STATE" = updated ] && _what="turned on (old config.yaml kept as config.yaml.bak)"
if run_step "Checking the Zabbix API" check_zabbix_api; then
if [ "$ZBX_HOSTS" = 0 ]; then
warn "Zabbix API" "$_what, but the token can read 0 hosts"
info "Give the token's user Read on your host groups (AlertINT > Settings > Connect again does it for alertint-svc)."
else
ok "Zabbix API" "$_what: $ZABBIX_URL, the token can read $ZBX_HOSTS hosts"
fi
else
warn "Zabbix API" "$_what, but the check failed: $ZBX_CHECK"
case $ZBX_CHECK in
*uthoriz*|*ession*) info "Wrong token, or Apache drops the Authorization header: add 'CGIPassAuth On' to the Zabbix Apache config." ;;
*) info "AlertINT reaches Zabbix at the same address from this host. Check ZABBIX_URL and the network, then run again." ;;
esac
fi ;;
manual)
warn "Zabbix API" "$CONF_DIR/config.yaml has a shape this installer does not edit; add it by hand:"
info "zabbix:"
info " api:"
info " base_url: \"$ZABBIX_URL\""
info " api_token_env: ZABBIX_API_TOKEN"
info "The token is already in $CONF_DIR/alertint.env. Then: sudo systemctl restart alertint" ;;
no_token)
warn "Zabbix API" "ZABBIX_URL given without ZABBIX_API_TOKEN; skipped" ;;
no_url)
warn "Zabbix API" "a token is set, but config.yaml has no Zabbix API yet"
info "Run again with ZABBIX_URL=<your Zabbix frontend address> to turn it on." ;;
esac
}
# What is left to do in Zabbix, with the values to paste.
next_steps() {
_n=1
printf '\n %sFinish in Zabbix%s, signed in as a Super admin:\n\n' "$B" "$R"
if [ "$MODULE" = installed ]; then
printf ' %s%s.%s Administration > General > Modules: press %sScan directory%s, enable %sAlertINT%s.\n' "$B" "$_n" "$R" "$B" "$R" "$B" "$R"
else
printf ' %s%s.%s Install the AlertINT module on your Zabbix frontend:\n' "$B" "$_n" "$R"
info "https://github.com/$MODULE_REPO#install"
info "Then Administration > General > Modules: Scan directory, enable AlertINT."
fi
_n=$((_n + 1))
printf ' %s%s.%s AlertINT > Settings, section %sAgent%s: enter these two, then %sSave%s.\n' "$B" "$_n" "$R" "$B" "$R" "$B" "$R"
printf ' Agent MCP address %s%s%s\n' "$B" "$MCP_URL" "$R"
printf ' MCP token %s%s%s\n' "$B" "$MCP_TOKEN" "$R"
_n=$((_n + 1))
printf ' %s%s.%s Section %sConnect Zabbix%s: enter this one, then %sConnect%s.\n' "$B" "$_n" "$R" "$B" "$R" "$B" "$R"
printf ' Webhook token %s%s%s\n' "$B" "$WEBHOOK_TOKEN" "$R"
_n=$((_n + 1))
printf ' %s%s.%s Press %sSend test problem%s. About a minute later AlertINT shows its finding.\n' "$B" "$_n" "$R" "$B" "$R"
info "The tokens are also in $CONF_DIR/alertint.env (root only)."
}
# ---------- main --------------------------------------------------------------
main() {
setup_ui
START=$(date +%s)
LOG=$(mktemp "${TMPDIR:-/tmp}/alertint-install.XXXXXX.log") || LOG=/dev/null
trap '[ -n "${SPIN_PID:-}" ] && kill "$SPIN_PID" 2>/dev/null; rm -rf "${WORK:-}"' EXIT
KEYWARN=0; REUSED=0; MODULE=none; ZBX_API_STATE=""
for a in "$@"; do
case $a in
--help|-h) sed -n '2,17p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//'
printf 'Help: %s\n' "$SELF_URL"; exit 0 ;;
--uninstall) DO_UNINSTALL=1 ;;
*) die "Unknown option: $a" "Try --help." ;;
esac
done
banner
have curl || die "curl is required" "Install curl and run again."
have tar || die "tar is required" "Install tar and run again."
detect_platform
ok "Detected" "$PRETTY"
if [ "${DO_UNINSTALL:-0}" = 1 ]; then uninstall; return; fi
setup_root
if [ "$OS" = linux ] && [ "$HAS_SYSTEMD" = 0 ]; then
die "systemd not found" "Automatic setup needs systemd. Download the binary from https://github.com/$REPO/releases and run: alertint serve --config <file>"
fi
if [ -n "${ZABBIX_URL:-}" ]; then
normalize_zabbix_url || die "ZABBIX_URL is not a web address" "Use the Zabbix frontend address, e.g. ZABBIX_URL=https://zabbix.example.com"
fi
# Key is needed on Linux (full install); on macOS we only fetch the binary.
# A re-run keeps the key already in alertint.env instead of asking again.
if [ "$OS" = linux ] && [ -z "${ALERTINT_TRIAL_KEY:-}${KEY:-}" ] &&
[ -n "$(env_get ANTHROPIC_API_KEY "$CONF_DIR/alertint.env")" ]; then
ok "Key" "keeping the one in $CONF_DIR/alertint.env"
elif [ "$OS" = linux ]; then
if ! get_key; then
if ! $SUDO test -f "$CONF_DIR/alertint.env" 2>/dev/null; then
die "No key given" "Run: curl -fsSL $SELF_URL | ALERTINT_TRIAL_KEY=YOUR_KEY sh"
fi
fi
[ "$KEYWARN" = 1 ] && warn "Key" "doesn't look like an Anthropic key (sk-ant-...); continuing"
fi
latest_tag || die "Could not find the latest release" "Check your network, or pin one: ALERTINT_VERSION=v0.16.0"
ok "Release" "$TAG"
run_step "Downloading $TAG ($OS/$ARCH)" download_and_verify ||
die "Download or checksum verification failed" "Details: $(tail -n 3 "$LOG" | tr '\n' ' ')"
ok "Downloaded" "alertint_${TAG#v}_${OS}_${ARCH}.tar.gz ($((SIZE / 1048576)) MB, SHA-256 checked)"
run_step "Installing binary" install_binary ||
die "Could not install to $BIN_DIR" "$(tail -n 3 "$LOG" | tr '\n' ' ')"
ok "Installed" "$BIN_DIR/alertint"
if [ "$OS" = darwin ]; then
warn "macOS" "binary installed; automatic service setup is Linux-only"
info "Run it with: ANTHROPIC_API_KEY=... alertint serve --config <config.yaml>"
printf '\n'; return
fi
run_step "Creating service user and folders" ensure_user_dirs ||
die "Could not prepare $CONF_DIR / $DATA_DIR" "$(tail -n 3 "$LOG" | tr '\n' ' ')"
run_step "Writing configuration" write_config ||
die "Could not write configuration" "$(tail -n 3 "$LOG" | tr '\n' ' ')"
if [ "$REUSED" = 1 ]; then
ok "Configured" "kept existing $CONF_DIR (tokens unchanged)"
else
ok "Configured" "$CONF_DIR/config.yaml (key stored 0640, root + alertint only)"
fi
run_step "Starting service" write_unit ||
die "Could not start the service" "See: journalctl -u alertint -n 30"
run_step "Waiting for the agent to answer" wait_healthy ||
die "Agent did not become healthy within 30s" "See: journalctl -u alertint -n 30"
ok "Running" "alertint service, starts on boot (webhook :$WEBHOOK_PORT, MCP :$MCP_PORT)"
report_zabbix_api
# The frontend on this host reaches the agent locally; anywhere else, by IP.
if find_modules_dir; then
if run_step "Installing the Zabbix module" install_module; then
MODULE=installed
ok "Module" "AlertINT ${MODULE_VERSION:-} in $MODULES_DIR/alertint"
else
warn "Module" "could not install it here (details: $LOG)"
fi
MCP_URL="http://127.0.0.1:$MCP_PORT/mcp"
else
ok "Module" "no Zabbix frontend on this host; install the module there"
MCP_URL="http://$(guess_host):$MCP_PORT/mcp"
fi
END=$(date +%s)
printf '\n %sDone in %ss.%s AlertINT is running.\n' "$B$GRN" "$((END - START))" "$R"
case "$REUSED:$ZBX_API_STATE" in
1:added|1:updated|1:unchanged)
# A re-run for the Zabbix API (the Settings page button): Zabbix is set up already.
printf '\n In Zabbix: %sAlertINT > Settings%s, section 4, press %sCheck%s.\n' "$B" "$R" "$B" "$R"
info "Values for the other sections, if you still need them: $CONF_DIR/alertint.env (root only)." ;;
*) next_steps ;;
esac
printf '\n'
info "Logs: journalctl -u alertint -f"
info "Uninstall: curl -fsSL $SELF_URL | sh -s -- --uninstall"
printf '\n'
return 0
}
main "$@"
Want a walkthrough? Read the Zabbix AI triage tutorial. Every option and the manual setup: Zabbix docs.
We set it up, keep it running in your network, and review the results with you.
Book a 30-minute call →