All Articles
DevOps··10 min read

Integrating AI Agents with uptimyctl: Let Claude Code Run Your Monitoring

Connect an AI agent to your uptime monitoring with a CLI, not an MCP server. Latency analytics for evidence-based diagnosis, CI recipes for deploy verification, incident response that posts status page updates in minutes, and a paste-ready CLAUDE.md snippet.

Your coding agent already reads your logs, greps your codebase, and writes your migrations. But ask it "is checkout actually slow right now, or is that ticket stale?" and it shrugs. It can see your code; it can't see your production. This guide fixes that with uptimyctl, the upti.my CLI: your agent gets read access to live health data, verified monitoring coverage on every deploy, a reviewed path to changing configuration, and the ability to run incident comms, from the first status page update to the post-mortem. It works the same whether the agent is Claude Code, Cursor, or something you built yourself.

Why a CLI beats an MCP server for this job

The reflexive answer to "connect an AI agent to a service" in 2026 is an MCP server. For monitoring, a CLI is usually the better interface. MCP tool schemas are loaded into the agent's context up front, whether or not it uses them, and a monitoring server with twenty tools costs you thousands of tokens on every single session. A CLI costs nothing until invoked: the agent runs uptimyctl --help once when it needs to, or better, you document the five commands that matter in your CLAUDE.md (there's a paste-ready snippet below).

The other reason is composability. uptimyctl takes a global -o json flag, so every command produces machine-readable output that pipes straight into jq, shell loops, and CI assertions. Agents are already good at shell. Give them a well-behaved binary and they'll chain it in ways no tool schema anticipated. Our CLI documentation covers the commands; this article is the practice.

Setup: install and scoped authentication

uptimyctl is open source (github.com/uptimy/uptimyctl), which matters more than usual here: an agent that can read the source never has to guess what a command does. Install with the release script, or via Go:

terminal
# Linux / macOS install script
curl -sSfL https://raw.githubusercontent.com/uptimy/uptimyctl/master/scripts/install.sh | sudo bash

# Or, with a Go toolchain
go install github.com/uptimy/uptimyctl@latest

Create an API key under Settings → API Keys in the upti.my dashboard (keys start with upt_). The CLI resolves credentials in a strict order: the --api-key flag wins, then the UPTIMYCTL_API_KEY environment variable, then the config file at ~/.config/uptimyctl/config.yaml written by uptimyctl auth login.

For agent environments, the environment variable is the right layer. A key in UPTIMYCTL_API_KEY never appears in the agent's command lines (so it can't leak into transcripts or shell history), doesn't require a config file baked into a sandbox image, and rotates by changing one secret. Interactive auth login is for your laptop; env vars are for everything automated.

terminal
export UPTIMYCTL_API_KEY="upt_..."

# Verify: reports the API URL, a masked key, and validity
uptimyctl auth status -o json

One key per agent

Give each agent context its own API key so you can revoke a single agent's access without touching your CI or your teammates. Keys are managed and revocable in the dashboard.

Give your agent eyes: reading production state

A handful of read commands cover most of what an agent needs to reason about production:

terminal
# Every monitor: UUID, application, interval, active state
uptimyctl healthchecks list -o json

# Full config and recent results for one check
uptimyctl healthchecks get <uuid>

# Open and recent incidents, filterable
uptimyctl incidents list -o json --status Ongoing

# Aggregate incident statistics for trend questions
uptimyctl incidents stats

# Application details (status, description, linked checks)
uptimyctl applications get <uuid>

And when the question is about performance rather than up-or-down, the analytics command group exposes execution history and latency rollups per check, at every granularity from raw executions to monthly aggregates:

terminal
# Individual executions (default range: last 24 hours)
uptimyctl analytics executions <hc-uuid> -o json

# Per-minute latency rollups by region, custom range
uptimyctl analytics minute-region <hc-uuid> \
  --from 2026-07-09T13:00:00Z --to 2026-07-09T15:00:00Z -o json

# Where the time went: per-minute latency phase breakdown
uptimyctl analytics minute-breakdown <hc-uuid> -o json

# Zoom out for trends
uptimyctl analytics hour-region <hc-uuid>
uptimyctl analytics daily-region <hc-uuid>
uptimyctl analytics monthly-region <hc-uuid>

Here's what that looks like in practice. You ask Claude Code "why is checkout slow?" and instead of guessing from code, it investigates:

agent-session.txt
> Why is checkout slow? Users started complaining around 14:00.

⏺ Bash(uptimyctl healthchecks list -o json | jq '.results[]
      | select(.application.name | test("checkout"; "i"))
      | {uuid, interval: .intervalSeconds, active}')
  ⎿ { "uuid": "9f2c...", "interval": 60, "active": true }

⏺ Bash(uptimyctl analytics minute-region 9f2c... \
      --from 2026-07-09T13:00:00Z --to 2026-07-09T15:00:00Z \
      -o json)
  ⎿ ~180ms until 13:52, then 2400-3900ms from every region.
    Status still 200 throughout. No incident opened: the
    check passes, it's just slow.

⏺ Bash(uptimyctl analytics minute-breakdown 9f2c... -o json)
  ⎿ The jump is almost entirely time-to-first-byte. Not DNS,
    not TLS: the server is sitting on requests.

⏺ Bash(uptimyctl incidents list -o json --status Ongoing)
  ⎿ One ongoing incident: "payments-db connection pool
    saturation", opened 13:49, severity high.

⏺ Checkout isn't broken, it's waiting. TTFB jumped 13x at
  13:52 in every region, three minutes after the payments-db
  pool incident opened. Checkout calls payments synchronously;
  that's your bottleneck. Want me to pull the incident
  timeline?

No dashboard tab, no screenshot pasted into the chat. The agent correlated latency data with the incident timeline and answered with evidence. That's the payoff of machine-readable monitoring: the same numbers you'd squint at on a graph become inputs the agent can reason over.

If you need a fresh data point, uptimyctl healthchecks trigger <uuid> requests an immediate execution, asynchronously: it queues the run and returns a confirmation, not the result. Useful for kicking a check after a fix; not usable as a synchronous assertion, which matters in the next section.

Recipe: verify monitoring coverage on every deploy

Every team eventually ships a service that nobody remembered to monitor, and discovers it during the outage. The fix is to make CI assert coverage: after each deploy, check that a monitor exists for every service you just shipped. And while the deploy is in flight, open a maintenance window so restarts don't page anyone.

.github/workflows/deploy.yml
name: Deploy and verify monitoring

on:
  push:
    branches: [main]

env:
  UPTIMYCTL_API_KEY: ${{ secrets.UPTIMYCTL_API_KEY }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uptimyctl
        run: |
          curl -sSfL https://raw.githubusercontent.com/uptimy/uptimyctl/master/scripts/install.sh | sudo bash

      - name: Open maintenance window
        id: maint
        run: |
          START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
          FINISH=$(date -u -d '+30 minutes' +%Y-%m-%dT%H:%M:%SZ)
          MAINT_UUID=$(uptimyctl maintenances create \
            --start-at "$START" \
            --finish-at "$FINISH" \
            --description "Deploy ${{ github.sha }}" \
            -o json | jq -r '.uuid')
          echo "uuid=$MAINT_UUID" >> "$GITHUB_OUTPUT"

      - name: Deploy
        run: ./scripts/deploy.sh

      - name: Assert every service has a monitor
        run: |
          uptimyctl healthchecks list -o json > checks.json
          MISSING=0
          for SVC in checkout-api payments-api web-frontend; do
            COUNT=$(jq --arg svc "$SVC" \
              '[.results[] | select(.active and (.application.name == $svc))] | length' \
              checks.json)
            if [ "$COUNT" -eq 0 ]; then
              echo "::error::No active monitor found for $SVC"
              MISSING=1
            fi
          done
          exit $MISSING

      - name: Close maintenance window
        if: always()
        run: uptimyctl maintenances resolve "${{ steps.maint.outputs.uuid }}"

Three details worth noting. maintenances create prints the created window as JSON, so jq -r '.uuid' captures the ID for the resolve step; resolve takes that UUID as its argument. The timestamps use GNU date syntax because Actions runners are Linux. And the assertion step is deliberately a coverage check, not a creation step. The CLI can create monitors (healthchecks upsert and bulk, both spec-driven), but an unattended CI job inventing configuration is how you end up with monitors nobody recognizes. Fail the build and let a human create the check deliberately, through the reviewed paths at the end of this article.

⚠️

Don't assert freshness with trigger

It's tempting to add healthchecks trigger here and then read the "latest" result as proof the deploy is healthy. Trigger is fire-and-forget: it returns a confirmation that the run was requested, and the execution lands whenever it lands. Asserting on the last stored result right after triggering races the check itself. Assert coverage in CI; let the monitor assert health on its own schedule.

Recipe: incident response that keeps customers informed

The second place agents earn their keep is during incidents, and not just as a reader. The CLI covers the full incident lifecycle: open an incident, publish it to status pages, post status-transition updates, resolve with a closing note, attach a post-mortem. Which means the slowest part of most incidents, telling your customers what's happening, no longer has to wait for a human to find the status page admin.

Wire an upti.my workflow to notify your agent (a webhook endpoint it listens on, or the Slack channel it watches), and the response to an alert can start with communication:

terminal
# 1. Alert arrives. Open an incident, published to the status
#    page in the same command.
uptimyctl incidents create \
  --title "Elevated checkout latency" --severity high \
  --description "p95 latency 13x baseline since 13:52 UTC" \
  --status-page <status-page-uuid> -o json

# 2. Narrate the investigation. --status transitions the incident:
#    Created, Acknowledged, Investigating, Identified, Monitoring, Resolved
uptimyctl incidents add-update <uuid> \
  --message "Investigating elevated checkout latency." \
  --status Investigating

uptimyctl incidents add-update <uuid> \
  --message "Cause identified: payments-db connection pool saturation. Fix rolling out." \
  --status Identified

# 3. Resolve with a closing note for status page followers
uptimyctl incidents resolve <uuid> \
  --message "Pool limits raised, latency back to baseline since 15:04 UTC."

# 4. Attach the post-mortem once it's written (posts it as a
#    public update; add --no-public-update to skip that)
uptimyctl incidents post-mortem <uuid> -f post-mortem.md

For context while debugging, incidents get returns the full picture (updates, status history, linked status pages) and incidents updates <uuid> lists the timeline. publish/unpublish add or remove status pages after creation without touching anything else. And internal notes stay internal: add-update defaults to --public=true, so raw debugging notes should pass --public=false explicitly.

💡

Which incidents can the CLI resolve?

Only manually created incidents (the kind the agent opened above) resolve through the CLI. Incidents opened automatically by monitoring resolve themselves when the underlying check recovers, which is exactly what you want: an agent can never talk a monitoring incident closed while the check is still failing.

The pattern that works is reads are free, writes are proposed. Let the agent pull incidents get whenever it wants; the worst outcome of a read is a wasted token. For anything that touches a public timeline, have the agent draft the command and show it to you before running. In Claude Code this is the default permission behavior for Bash; keep it that way for these verbs. An agent that writes a wrong root cause onto a public status page has done real damage. The speed win isn't skipping the human, it's that the human reviews a ready-made update instead of composing one at 3 a.m.

Recipe: propose alert rules from evidence

Diagnosis usually ends with "we should have been alerted sooner". That, too, is now scriptable. Alert rules and status pages have full CRUD in the CLI, and both use the same agent-friendly shape: a JSON spec, passed with -f (or piped on stdin with -f -). The agent composes the spec from the evidence it just gathered; you read it before it runs.

terminal
# What alerting exists on this application today?
uptimyctl alert-rules list --application <app-uuid> -o json

# The agent drafts a rule from the analytics it just read:
cat > latency-rule.json <<'EOF'
{
  "name": "Checkout p95 latency",
  "alertRuleType": "latency",
  "latencyThresholdMS": 1000,
  "thresholdFailures": 3,
  "timeWindowMinutes": 5,
  "severity": "warning"
}
EOF

# You approve, it applies
uptimyctl alert-rules create --application <app-uuid> -f latency-rule.json

In the checkout story above, this is the closing move: latency sat 13x above baseline for over an hour with no page, because the check only asserted up-or-down. The agent that found the gap can propose the rule that closes it, with a threshold justified by the actual latency distribution it pulled from analytics, not a number pulled from the air.

Status pages work the same way: uptimyctl status-pages list/get/create/update/delete, spec-driven, plus a groups subcommand for arranging components. Useful for keeping page structure in version control, or letting an agent draft a page for a new environment. As always, creation and deletion belong behind an approval prompt.

Paste-ready CLAUDE.md snippet

Drop this into your CLAUDE.md or AGENTS.md and your agent knows the interface, the output format, and the rules:

CLAUDE.md
## Monitoring (uptimyctl)

We monitor production with upti.my. The `uptimyctl` CLI is installed
and authenticated via the UPTIMYCTL_API_KEY environment variable.
Add `-o json` to any command for machine-readable output.

### Read freely (no approval needed)
- `uptimyctl healthchecks list -o json`   : all uptime checks and their state
- `uptimyctl healthchecks get <uuid>`     : one check's config and results
- `uptimyctl incidents list -o json`      : filter with --status / --severity
- `uptimyctl incidents get <uuid>`        : details, updates, status history
- `uptimyctl incidents updates <uuid>`    : an incident's timeline updates
- `uptimyctl incidents stats`             : aggregate incident statistics
- `uptimyctl applications list -o json`   : all monitored applications
- `uptimyctl maintenances list -o json`   : scheduled maintenance windows
- `uptimyctl analytics executions|minute-region|minute-breakdown|hour-region|daily-region|monthly-region <hc-uuid>`
    : latency history and phase breakdowns; --from/--to RFC3339, default last 24h
- `uptimyctl alert-rules list --application <app-uuid>` : alerting on an app
- `uptimyctl status-pages list`           : public status pages
- `uptimyctl export -f <file>`            : apps, checks, tags, alert rules as JSON

### Ask before running (show me the exact command first)
- `uptimyctl incidents create --title "..." --severity ... --status-page <uuid>`
- `uptimyctl incidents add-update <uuid> --message "..." [--status ...] [--public=false]`
- `uptimyctl incidents resolve <uuid> --message "..."`
- `uptimyctl incidents publish/unpublish <uuid> --status-page <uuid>`
- `uptimyctl incidents post-mortem <uuid> -f <file.md>`
- `uptimyctl maintenances create/update/start/resolve/cancel`
- `uptimyctl healthchecks upsert --application <app-uuid> -f <spec.json>`
- `uptimyctl healthchecks bulk -f <spec.json>`
- `uptimyctl healthchecks pause/resume/trigger <uuid>`
- `uptimyctl alert-rules create/update --application <app-uuid> -f <spec.json>`
- `uptimyctl status-pages create/update -f <spec.json>`
- `uptimyctl import <file>`

### Rules
1. NEVER run `healthchecks delete`, `maintenances delete`,
   `alert-rules delete`, or `status-pages delete`.
2. Prefer the export/import loop for monitor changes:
   `uptimyctl export -f config.json`, propose an edit as a diff,
   and only after I approve it, `uptimyctl import config.json`.
   Use `healthchecks upsert`/`bulk` only for one-offs I approve.
3. Only resolve incidents YOU created. Monitoring-created
   incidents resolve themselves when the check recovers; never
   try to talk one closed.
4. Incident updates default to --public=true and can land on the
   status page. Internal notes need --public=false explicitly.
5. `healthchecks trigger` is asynchronous. Never treat the next
   `get` result as the triggered run.
6. Heartbeat monitors are a SEPARATE feature not managed by this
   CLI. Do not invent heartbeat commands; heartbeats are configured
   in the upti.my app and pinged over HTTP.
7. Never print, echo, or commit UPTIMYCTL_API_KEY.
8. When citing monitoring data, include the check UUID and timestamp.

The rules section is the part most people skip and shouldn't. Agents fill gaps with plausible guesses, and "plausible" for a CLI means inventing subcommands that don't exist. Telling the agent explicitly what is not there saves you a confused loop of failed commands.

Safety and scoping

Three boundaries keep this arrangement boring, in the good way:

  • Read-only by default. Everything an agent needs for diagnosis is a read. Allowlist the list/get/stats commands in your agent's permissions and leave every mutating verb behind an approval prompt.
  • Explicit approval for create, resolve, and delete. The CLI can open and resolve incidents, publish them to status pages, create monitors and maintenance windows, and delete most of it. Each of those is one confirmation away from an agent, which is exactly as it should be: one confirmation, not zero.
  • Keys live in secrets, never in the repo. UPTIMYCTL_API_KEY belongs in GitHub Actions secrets, your sandbox's env config, or a secrets manager. Never in CLAUDE.md, never in a committed .env.

It also helps to know the blast radius. The CLI operates on your upti.my workspace: monitors, incidents, maintenances, alert rules, status pages, exported config. It cannot touch your actual infrastructure or restart services. The worst case of a misbehaving agent with a leaked key is corrupted monitoring state (including, since status pages are now writable, a defaced public status page, which is why their mutating verbs sit on the approval list). All of it is recoverable, especially if you follow the next section.

Everything as code: the export/import loop

The most agent-friendly feature in uptimyctl isn't a clever subcommand, it's that the whole workspace serializes to JSON:

terminal
# Snapshot applications, healthchecks, tags, and alert rules
uptimyctl export -f monitoring-config.json

# After human review of the diff, apply it
uptimyctl import monitoring-config.json

Commit that file to git and monitoring changes become code review. An agent that thinks a new service needs a monitor edits monitoring-config.json, opens a pull request, and a human reads the diff like any other config change. Import is idempotent (existing items are matched by name and skipped), so applying the reviewed file is safe to repeat. This is the same "propose, review, apply" loop your team already trusts for Terraform, extended to monitoring.

For one-off creation there's also the direct path: healthchecks upsert --application <app-uuid> -f check.json creates or updates a single application's check from a spec, and healthchecks bulk -f monitors.json creates several at once, all-or-nothing. Same rule as every other mutating verb: the agent drafts the spec, you read it, then it runs. For anything beyond a quick one-off, prefer the export/import loop; the file in git stays the source of truth.

It's also your recovery story: if an agent (or a human) ever mangles the workspace, uptimyctl import from the last committed snapshot puts it back. Where this fits in the bigger picture, alongside alert routing and status pages, is covered in how to build a reliability stack.

📌Key Takeaways

  • 1A CLI with -o json is a cheaper, more composable agent interface than loading MCP tool schemas into every session
  • 2Authenticate agents with the UPTIMYCTL_API_KEY env var: flag beats env beats config file, and env is the right layer for sandboxes and CI
  • 3healthchecks, incidents, and analytics reads give an agent evidence to diagnose with: latency rollups per minute, region, and phase
  • 4In CI, wrap deploys in a maintenance window and assert monitor coverage with jq; trigger is async, so never use it to assert freshness
  • 5Alert rules and status pages are spec-driven: the agent drafts the JSON from evidence, a human approves, -f applies it
  • 6Reads are free, writes are proposed: add-update, resolve, and every create/update stay behind human approval
  • 7An agent wired to alert notifications can open a published incident, narrate status transitions, and resolve with a closing note; monitoring-created incidents can't be talked closed, they resolve when the check recovers
  • 8Monitor changes flow through export, a reviewed diff, and import; upsert and bulk exist for approved one-offs

The setup is one API key and one CLAUDE.md paste. Sign up, grab a key, install the CLI, and the next time you ask your agent whether production is healthy, it will answer with data instead of a disclaimer.