Agents CLI: let AI coding agents fix Rust performance regressions

hotpath cloud is the command line client of the hotpath-cloud API. It gives a coding agent, a script or a CI step the same judgement the pull request comment shows, as JSON: which report was compared with which baseline, under which policy, what regressed, which budgets broke, and the verdict. An agent can push a change, wait for the benchmark, read the verdict and fix what regressed, without opening the dashboard.

Give your agent the hotpath-rs agent skill (npx skills add hotpath-rs/hotpath --skill hotpath-rs): it points the agent to the step-by-step workflow for these commands, waiting for a benchmark, reading the verdict and fixing what regressed.

Install and authenticate

cargo install hotpath --features cloud

Create a personal API token at hotpath.rs/app/tokens and export it:

export HOTPATH_API_TOKEN=...
hotpath cloud auth --pretty
{
  "login": "pawurb",
  "token": {
    "name": "laptop",
    "expires_at": "2026-12-25T11:27:12.311073Z"
  }
}

The token reads what your account reads on hotpath.rs. repos lists the repositories in the hotpath GitHub App installations your GitHub user can see, the same list as hotpath.rs/app, where you can also add more. The other commands also read the public reports of any other project when you name its repository; see which repositories you can read. The same data is available to AI assistants through the hotpath-cloud MCP server.

CLI commands

repos and benchmarks

repos lists the repositories of your installations, with their benchmarks. benchmarks lists one repository’s:

hotpath cloud benchmarks --repo hotpath-rs/hotpath --pretty
{
  "repository": "hotpath-rs/hotpath",
  "benchmarks": [
    { "name": "drain", "reports": 55, "latest_report_at": "2026-10-01T23:09:00.46559Z" },
    { "name": "meta", "reports": 54, "latest_report_at": "2026-10-01T23:09:29.367532Z" }
  ]
}

report

Fetches one stored report, selected by pull request, commit or id:

hotpath cloud report --repo hotpath-rs/hotpath --benchmark drain --pr 604 --no-payload --pretty
{
  "id": "01a0f476-5b3a-7d0e-901a-7756f28e2414",
  "event": "pull_request",
  "commit_sha": "ac5d8dcb4d21b714e0d2a04fd837637b3e1d1356",
  "head_ref": "perf/cycle-drain-buffer",
  "pr_number": 604,
  "baseline_id": "01a0f43f-c0f8-71c0-bfcc-ef82bbb17270",
  "comment_url": "https://github.com/hotpath-rs/hotpath/pull/604#issuecomment-5783640286",
  "regressed": false,
  "policy_path": "hotpath/drain-policy.toml",
  "dashboard_url": "https://hotpath.rs/app/repos/hotpath-rs/hotpath/benchmarks/drain/reports/01a0f476-5b3a-7d0e-901a-7756f28e2414"
}
FlagSelects
--pr Nthe newest report of the pull request
--commit SHAthe newest report of a full 40-character commit sha, measured or as the pull request’s head
--id IDthe report with this id
--event push|pull_requestnarrows --pr or --commit to one event

--commit $(git rev-parse HEAD) works on a pull request branch: CI measures GitHub’s merge commit, and the report also matches the pull request’s head commit. Without --no-payload the answer also carries the full uploaded hotpath report.

--select JSON filters the payload to the entities you name. The object names resources (functions, sql, http, server, mutexes, rw_locks, channels, io, threads), each with exact keys as the diff’s rows name them; an io value is named by its label and keeps all its operations, a thread name keeps every thread of that name:

hotpath cloud report --repo OWNER/NAME --benchmark NAME --pr 604 \
  --select '{"functions":{"keys":["app::hot","app::process"]},"server":{"keys":["GET /api/users"]}}'

The payload keeps its usual shape. Each kept section’s data holds only the matching entries, unchanged, so a function keeps its timing, allocation, instruction count and CPU entries, whichever the report has. included_count counts the kept entries, while total_count and the totals still describe the whole run. Sections of other resources, streams, futures, debug and sections left without entries are dropped, so a key that matches nothing keeps nothing. --select does not go with --no-payload. A selection names at most 20 keys; the server’s error says what to change in one it refuses, and a report too old for the server to read is an error rather than an unfiltered payload.

--precise returns measurements at the precision the uploaded report carries instead of rounded for reading: the payload exactly as uploaded (11.617504 ms, 2425213 B) and the run totals in nanoseconds and bytes (327925208 ns). It works with every other flag, --select included, and changes no other field. MCP and REST API lists the units.

A report that is not there yet answers not_found with exit code 1, so polling report until it exits 0 waits for CI’s upload.

reports

Lists a repository’s stored reports, newest first. Each entry is one upload with the same fields as report --no-payload, so a pull request measured three times lists three reports:

hotpath cloud reports --repo hotpath-rs/hotpath --benchmark drain --event push --branch main --limit 2 --pretty

Trimmed answer:

{
  "repository": "hotpath-rs/hotpath",
  "reports": [
    {
      "id": "01a1adfe-f27a-7af2-b715-73db19160507",
      "benchmark": "drain",
      "event": "push",
      "commit_sha": "d589d1576e78c5a22a75016c6ef3383fede162c2",
      "git_ref": "refs/heads/main",
      "regressed": false,
      "failed": false,
      "created_at": "2026-11-05T23:15:29.016432Z",
      "dashboard_url": "https://hotpath.rs/app/repos/hotpath-rs/hotpath/benchmarks/drain/reports/01a1adfe-f27a-7af2-b715-73db19160507"
    },
    {
      "id": "01a1adfb-0a75-746a-97af-5b4d58bc2977",
      "benchmark": "drain",
      "event": "push",
      "commit_sha": "8c171222afc5094e283fcaabf56b84ff02b81227",
      "git_ref": "refs/heads/main",
      "regressed": false,
      "failed": false,
      "created_at": "2026-11-05T23:11:13.011209Z",
      "dashboard_url": "https://hotpath.rs/app/repos/hotpath-rs/hotpath/benchmarks/drain/reports/01a1adfb-0a75-746a-97af-5b4d58bc2977"
    }
  ],
  "next_cursor": "AQGhrfsKdXRql69bTVi8KXc"
}

Every filter is optional, and filters combine:

FlagLists only reports
--benchmark NAMEof this benchmark; without it, of every benchmark of the repository
--event push|pull_requestuploaded from this CI event
--pr Nof the pull request
--commit SHAof a full 40-character commit sha, measured or as the pull request’s head; with --pr, that pull request’s reports at that commit
--branch NAMEmeasured on this branch (refs/heads/NAME), given as the bare name; a pull request’s head and base branches do not match
--since DATEuploaded at or after DATE
--until DATEuploaded before DATE
--verdict VERDICTwith this stored verdict: passed (judged, no regression), regressed (judged with a regression, failed reports included), failed, not_judged
--metadata KEY=VALUEwhose user_metadata has KEY with exactly VALUE; repeat it (up to 8 times) to require several pairs

DATE is YYYY-MM-DD, read as midnight UTC, or an RFC 3339 time such as 2026-11-01T10:30:00+01:00.

--metadata matches the pairs a report was uploaded with (HOTPATH_USER_METADATA or HotpathGuardBuilder::user_metadata), case-sensitively; a report without metadata never matches. It splits on the first =, so a value may contain = or ,. To compare one benchmark across runners:

hotpath cloud reports --repo hotpath-rs/hotpath --benchmark drain --metadata runner=Linux-ARM64 --limit 10

--select JSON lists each report exactly as report --select prints it, with its payload filtered to the entities you name, to follow a few functions or routes across a benchmark’s history. A page then holds 1 to 20 reports (default 10). --precise returns each report’s totals and payload as report --precise does.

Each run prints one page of --limit N reports (1 to 50, default 20). When more reports match, next_cursor is a string: pass it as --cursor with the same filters to get the next page. It is null on the last page.

hotpath cloud reports --repo hotpath-rs/hotpath --benchmark drain --limit 20 --cursor "AQGhrfsKdXRql69bTVi8KXc"

A listing always exits 0, also when it is empty or lists regressed or failed reports. Use diff for an exit code that follows a verdict.

diff

Compares a report with its baseline under the policy the report carried, exactly as the pull request comment did. It selects the report with the same flags as report:

hotpath cloud diff --repo hotpath-rs/hotpath --benchmark drain --pr 604 --pretty

Trimmed answer:

{
  "rows": "findings",
  "verdict": {
    "judged": true,
    "regressed": false,
    "regressions": 0,
    "improvements": 1,
    "budgets_broken": 0
  },
  "budgets": { "rules": 1, "broken": 0, "findings": [], "notes": [] },
  "result": {
    "status": "compared",
    "sections": [
      {
        "resource": "functions",
        "kind": "alloc",
        "family": { "name": "alloc", "judged": true, "min_percent_change": 2.0, "metrics": ["avg", "total"] },
        "rows": [
          {
            "name": "hotpath_drain::EventProducer::push",
            "location": { "file": "crates/hotpath-drain/src/lib.rs", "line": 194 },
            "outcome": "improvement",
            "cells": [
              { "column": "avg", "unit": "bytes", "base": "24 B", "head": "1 B", "change_percent": -95.83, "crossed": "down" },
              { "column": "total", "unit": "bytes", "base": "23.1 MB", "head": "1.2 MB", "change_percent": -94.98, "crossed": "down" }
            ]
          }
        ]
      }
    ]
  },
  "dashboard_url": "https://hotpath.rs/app/repos/hotpath-rs/hotpath/benchmarks/drain/reports/01a0f476-5b3a-7d0e-901a-7756f28e2414/diff"
}

How to read it:

  • verdict covers the comparison and the budgets together. judged says whether anything was judged, regressed whether a judged section regressed or a budget broke.
  • result.status is compared, no_baseline or unreadable. Budgets are checked without a baseline too.
  • Each section is one section of the regression policy. A row’s outcome is regression, improvement, added or removed, and its cells are the values that explain it. A cell with crossed went past the policy’s min_percent_change, up or down.
  • Values are strings formatted as the comment shows them, with their unit. change_percent is the exact number to reason about.
  • budgets.findings lists the broken budget checks, each with its entry, actual, limit and the rule’s message.

By default the answer lists only what the comment lists. --advisory adds the findings of sections the policy lists with judged = false, which never count toward the verdict. --full lists every row and every value.

--select JSON keeps only the rows of the entities you name, with the same object as report --select:

hotpath cloud diff --repo OWNER/NAME --benchmark NAME --pr 604 \
  --select '{"functions":{"keys":["app::hot"]},"io":{"keys":["sock"]}}'

Alone it lists every row of those entities, as --full does, including unchanged rows and rows the policy ignores; with --advisory only their findings. An io label keeps one row per operation. A section left without rows is dropped, unless it still names a selected entity in omitted_from_base or omitted_from_head (cut by a truncated report), and a section the policy leaves out is never added. The verdict, the budgets, the totals, the counts and the exit code describe the whole comparison, the same as without --select.

--precise writes every value of the diff unrounded: cells, totals and budget limits in nanoseconds and bytes, throughputs with their uploaded tenth of a byte (12.4 B), and change_percent without rounding, so a change too small to show keeps its value. It never changes which rows are listed, their outcomes, the verdict or the exit code.

diff exits 0 only when the report was judged and nothing regressed. A regression, a broken budget, or nothing to judge (no baseline and no budgets) exits 1 with the answer on stdout.

validate-policy and init

init writes the default policy to hotpath/policy.toml, and validate-policy checks a policy file before you push it. See regression policy.

hotpath cloud init
hotpath cloud validate-policy --pretty

init --benchmark NAME writes hotpath/NAME-policy.toml, the policy of that benchmark alone, and validate-policy --benchmark NAME checks the file a run of that benchmark picks. init refuses to replace an existing file unless you pass --force.

Output and exit codes

Every command prints one JSON document on stdout, compact by default, indented with --pretty, or written to a file with --output FILE. An error prints one JSON document on stderr and nothing on stdout:

{"error":"No report for PR #999999 in benchmark drain.","code":"not_found"}
Exit codeMeaning
0success, for reports whatever verdicts the page lists; for diff, the report was judged and nothing regressed
1an error (not found, auth, network, invalid argument value), or for diff any answer that is not “judged and fine”
2usage error