AlertINT / for Zabbix01
AlertINT

Event offer · 100 free credits

Read the cause, not the alert.

AlertINT reads each Zabbix problem and posts the likely cause to Slack, ntfy or your AI agent.

An AlertINT finding in Slack: checkout PlaceOrder errors may stem from payment service failures, with the alert state, alert age and investigation timing.
Fig. 1 — Real lab capture, checkout incident finding. Swipe sideways to read it.

100 free credits.

To try AlertINT on your own alerts, while the event pool lasts.

  1. Register

    Just your email.

    How we handle your email: privacy policy

  2. Get your key

    It arrives by email.

  3. Run the install

    Pass the key in the command.

    curl -fsSL https://alertint.com/zabbix/install.sh | \ALERTINT_TRIAL_KEY=YOUR_KEY_FROM_EMAIL sh
    Paste the key from your email in place of YOUR_KEY_FROM_EMAIL.

Six alerts. One finding.

Sample with invented hosts and cause. Grouping across hosts needs a shared trigger tag (here service=checkout) and one config line.

Zabbixsends 6 problems in 2 minutes
  • web-01Response time above 2 s
  • web-02Response time above 2 s
  • app-01HTTP 5xx rate above 5%
  • app-02HTTP 5xx rate above 5%
  • db-01PostgreSQL connections at 95%
  • db-01Disk space is low on /var/lib/postgresql
AlertINTgroups them and finds the cause

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

  • app-01, app-02: 5xx errors
  • web-01, web-02: slow responses
  • db-01: connections at 95%

Evidence

Disk free fell from 31% to 0% in 40 minutes, after the 13:48 config change.

Then, in Zabbix.

  1. Enable the module

    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.

  2. Paste three values

    AlertINT, Settings. The script prints the agent address and two tokens to paste.

  3. Connect, then test

    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.

See what the script prints
$ 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.

See what Zabbix sends
Zabbix media type parameters
ParameterValue
urlhttp://<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.

Optional: let AlertINT read Zabbix history

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 sh

It 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):

  1. 1.User roles, Create role. Set API access to an allow list of only: host.get, hostgroup.get, trigger.get, problem.get, event.get, item.get, history.get, trend.get.
  2. 2.User groups, Create user group. Give it Read permission on the host groups AlertINT should see.
  3. 3.Users, Create user. Assign that role and that group.
  4. 4.Users, API tokens, Create API token for that user. Use it as ZABBIX_API_TOKEN.

With that role, even a leaked token cannot change anything in Zabbix.

Test it before a real alert
$ 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":""}'204

Real requests: 401 without the token, 204 with it.

Read the whole script
#!/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.

It never writes to Zabbix.
It only reads, through an API role you limit to seven read methods.
It finds the cause. You decide the fix.
AlertINT does the investigating, so your team starts from the answer instead of the alert. Changes to your systems stay with you.
In the trial, alert details go to the AI provider behind your key.
Use test alerts or non-sensitive hosts while you try it.

Or have us run it.

We set it up, keep it running in your network, and review the results with you.

Book a 30-minute call →