AI-powered incident investigation is an LLM agent that takes an alert, runs read-only queries against the live system (kubectl, logs, metrics), picks each next query from what the last one returned, and stops when the evidence supports one cause. It then reports that cause with the evidence behind it and a suggested fix that a person approves. It does not replace your monitoring or paging: it does the first pass of an engineer's diagnosis, and it can be wrong in ways you can only catch by reading its trace.
Below are two real investigations of the same fault, one by Aurora and one by HolmesGPT, with every tool call in order. Then the scenario where both agents failed, what happened when their commands were blocked, and what to require from any tool. The faults come from our lab: 19 injected Kubernetes failures, two runs each, all public in Arvo-AI/aurora-lab and summarized on /benchmark. Arvo AI builds Aurora and wrote the scenarios. For a list of the products in this category, see the AI incident investigation tools list; this page is about how the method works.
How does an AI agent investigate an incident?
Every agent we have traced runs the same loop:
- Read the alert and anything it already knows (past incidents, memory, connected clusters).
- Look wide: pods, events, logs of the alerted service.
- Follow the first anomaly in the output, for example an error line in a log.
- Rule out the obvious suspects (is the dependency down, is the node full).
- Find the object that explains the anomaly and confirm it with a second piece of evidence.
- Report: cause, evidence, what it ruled out, a suggested fix.
The model decides each step. Nothing in the loop forces it to look beyond what the alert points at, which is where it fails (see scenario 12 below).
What did Aurora do, call by call?
Scenario 13 of the lab, run r1, 2 October 2026. The fault: a NetworkPolicy named redis allows ingress to redis on 6379 only from pods labeled app=worker. The api pods are not allowed, so every /api/items request times out on its redis call and returns 503. Every pod stays Running and Ready, because the api's /healthz does not touch redis. The alert:
HighErrorRate [critical] namespace=shop service=frontend: 100% of requests to /api/ returned 5xx over the last 5 minutes (threshold 5%).
Aurora (commit 87d22baf, model Claude Sonnet 5 on Bedrock) had a fresh user and organization, so no memory of earlier runs, and the lab's admin kubeconfig behind its default read-only observability_only command policy. These are all 25 tool calls, start times in UTC, from steps-full.json in the run folder. Indented lines are copied from the tool output, with long lines cut at the right; [...] marks skipped lines, and other bracketed lines are our notes.
20:24:50 list_incidents, grep_memories "frontend", get_connected_clusters
No matches for 'frontend' in memory.
20:24:54 kubectl get pods -n shop -o wide
api-f64486c4c-2nqsk 1/1 Running 0 73s
redis-59458f55d8-n6glm 1/1 Running 0 73s
[... all 7 pods Running, 0 restarts]
20:24:54 kubectl get events -n shop --sort-by=.lastTimestamp
20:24:54 grep_memories "aurora-lab"
20:25:07 kubectl logs -n shop -l app=frontend --tail=100
20:25:07 kubectl logs -n shop -l app=api --tail=100
redis connection failed (attempt 1/5): [Errno 111] Connection refused
connected to redis at redis:6379
api listening on :8080
[...]
request failed: redis error: timed out
10.244.2.45 "GET /items HTTP/1.0" 503 -
[... repeats, with /healthz returning 200 in between]
20:25:07 kubectl get configmap -n shop
20:25:25 kubectl logs -n shop -l app=redis --tail=100
1:M 02 Oct 2026 20:23:45.678 * Ready to accept connections tcp
20:25:25 kubectl describe pod -n shop -l app=redis
20:25:25 kubectl top pods -n shop
redis-59458f55d8-n6glm 3m 4Mi
20:25:40 kubectl logs -n shop -l app=worker --tail=100
20:25:40 kubectl get configmap api-code -n shop -o yaml
with socket.create_connection((REDIS_HOST, 6379), timeout=2) as s:
20:25:40 kubectl get svc -n shop
20:26:01 kubectl top nodes
20:26:01 kubectl describe pod -n shop -l app=worker
20:26:01 kubectl get nodes -o wide
20:26:33 kubectl get pod -n shop -l app=loadgen -o yaml
20:26:33 kubectl get networkpolicies -n shop
NAME POD-SELECTOR AGE
redis app=redis 2m49s
20:26:33 kubectl get deployment api -n shop -o yaml
20:26:49 kubectl describe networkpolicy redis -n shop
Created on: 2026-10-02 20:23:54 +0000 UTC
[...]
PodSelector: app=redis
Allowing ingress traffic:
To Port: 6379/TCP
From:
PodSelector: app=worker
20:26:49 kubectl get pod -n shop -l app=api --show-labels
api-f64486c4c-2nqsk 1/1 Running 0 3m8s app=api,pod-template-hash=f64486c4c
20:27:41 write_memory, again at 20:27:56: "shop namespace frontend 5xx caused by redis NetworkPolicy excluding api pods"
What turned the investigation, in order:
- The api log at 20:25:07.
redis error: timed outmoved the question from "why is the frontend failing" to "why can the api not reach redis". The first line, "Connection refused", is from startup before redis was up, and the api then connected. The later errors are timeouts, not refusals: the hint that packets are being dropped, not rejected. - Ruling redis out at 20:25:25. Clean startup log, Ready, 3m CPU. The agent now knows the dependency is up and the path to it is broken.
- The detour at 20:25:40 to 20:26:01. Worker logs, nodes, node usage. None of it was needed. It read the api source from the ConfigMap, which gave it the 2 second client timeout it later cited.
- The policy at 20:26:33.
get networkpolicieswas call 20 of 25, about 1 minute 40 seconds after the first call.describeshowed theapp=workerallow-list, and listing the api pods' labels confirmed they carryapp=api. Two pieces of evidence, one from each side of the blocked path.
The report's root cause:
The root cause was a NetworkPolicy named
redisin the shop namespace, created at 2026-10-02T20:23:54Z, which restricts ingress to redis:6379 to pods labeledapp=workeronly [20, 22]. Theapipods are labeledapp=api, notapp=worker[23], so their connections to redis were silently dropped by the policy [...]
The bracketed numbers point at the tool calls above, so each claim can be checked against its output. The report also had a "Ruled Out" list (redis overload, node contention, startup noise) and a "Not Checked" list. Separately, Aurora suggested a fix with a risk rating:
title: Patch `redis` NetworkPolicy to allow `app=api` ingress
risk: low
command: kubectl patch networkpolicy redis -n shop --type='json' -p='[{"op":"add","path":"/spec/ingress/0/from/-","value":{"podSelector":{"matchLabels":{"app":"api"}}}}]'
undo: null
Nothing ran it; suggestions wait for a person. Note the empty undo: to roll that patch back you would re-apply the original policy manifest. Run total: 202 seconds, 25 tool calls, 38 model calls, $0.69 in LLM cost. Graded correct. The model review still found two overstatements in the report: it called the young pod ages "a full cluster/deployment restart", and it said "100% failure" when one api log showed a 200 before the policy took effect. The NetworkPolicy blocking traffic runbook covers the fix by hand.
What did HolmesGPT do on the same fault?
HolmesGPT 0.43.0, same model, same cluster setup and fault, run on 15 October 2026 with holmes ask and its default toolsets. It got the same alert text with a slightly different suffix: "Namespace shop, cluster kind-aurora-lab. Find the root cause and say what changed." (Aurora's was "Cluster: aurora-lab (kubeconfig). Investigate and find the root cause."). HolmesGPT prints each call as it goes. Its 26 calls, grouped by model turn, from holmes.md in the run folder:
| Turn | Calls | What it asked for |
|---|---|---|
| 1 | #1 to #3 | frontend pods, deployments, Services |
| 2 | #4 to #6 | frontend pod logs, Services in shop, ConfigMap names |
| 3 | #7 to #9 | Service IPs, the frontend-nginx ConfigMap, pod status |
| 4 | #10 to #11 | full pod state, api deployments |
| 5 | #12 to #15 | logs of both api pods, the api-code ConfigMap, api env and ports |
| 6 | #16 to #18 | redis pod state and logs, kubectl top pods -A |
| 7 | #19 to #21 | events in shop, loadgen spec, node status (from the aks/node-health toolset, on a kind cluster) |
| 8 | #22 to #25 | kubectl get networkpolicies, top pods per node, worker resources |
| 9 | #26 | pod labels |
Its conclusion, after 110 seconds:
A NetworkPolicy named redis in namespace shop (created 2026-10-15T09:06:57Z, applied via kubectl apply) restricts ingress to the redis pod (redis-59458f55d8-kspkb, label app=redis) to only allow traffic from pods labeled app=worker, on TCP/6379
It then proposed adding app=api to the policy's from list and listed what it checked and found healthy. Graded correct. Its report says the policy was applied "4s later" than the api's first redis PING, which the judge noted is inferred from timestamps rather than shown.
The two investigations followed the same path: logs of the failing service, the api's redis timeout, redis ruled out, then the policy late in the run (call 20 of 25 for Aurora, 22 of 26 for HolmesGPT). Neither went to NetworkPolicies first, even though a timeout to a healthy pod in the same namespace points there.
| Scenario 13 | Aurora r1 | Aurora r2 | HolmesGPT r1 | HolmesGPT r2 | K8sGPT r1, r2 |
|---|---|---|---|---|---|
| Verdict | correct | correct | correct | correct | "No problems detected" |
| Time | 202 s | 444 s | 110 s | 118 s | under 1 s |
| Tool calls | 25 | 19 | 26 | 22 | analyzers only |
| LLM cost | $0.69 | $0.54 | not captured | not captured | not captured |
| Date | 2026-10-02 | 2026-10-02 | 2026-10-15 | 2026-10-16 | 2026-10-15/16 |
Aurora's grades were read by a person at Arvo; HolmesGPT's and K8sGPT's are judge-model only (details below). Aurora r2's 444 seconds includes waiting for a worker slot taken by Aurora's own background discovery jobs in the lab stack. K8sGPT does not take an alert: k8sgpt analyze scans the namespace for objects in a bad state, and with every pod Running and Ready its default analyzers found nothing. That is a different job, not a failed investigation.
Where does AI incident investigation fail?
Scenario 12 is the clearest failure we have. Two unrelated changes went out together: the worker's BATCH_SIZE went from 500 to 2500 (OOMKilled against a 192Mi limit), and the frontend Service's selector was changed to app=frontend,tier=web, which no pod carries. The alert covered only the worker:
KubePodCrashLooping [warning] namespace=shop deployment=worker: Pod shop/worker (container worker) is in waiting state (reason: CrashLoopBackOff).
| Scenario 12 | Tool calls | Found the OOM and the batch size change | Checked Services or endpoints | Said about the frontend |
|---|---|---|---|---|
| Aurora r1 | 18 | yes | no | "Other workloads in the shop namespace (api, frontend) remained healthy and unaffected [5, 14]." |
| Aurora r2 | 14 | yes | no | "Other workloads in the namespace (api, frontend pods) were running normally and unaffected [4]." |
| HolmesGPT r1 | 4 | yes | no | nothing |
| HolmesGPT r2 | 6 | yes | no | nothing |
| K8sGPT r1, r2 | analyzers | reported the OOMKill, not the batch size change | listed the Service | "Service has no endpoints, expected label tier=web" |
All four agent runs were graded partial. Both agents did good work on the alerted fault (Aurora in both runs and HolmesGPT in r2 worked out the memory per batch from the worker's code) and then stopped. Aurora's version is worse: in both runs it declared the frontend unaffected and cited get pods output as proof (calls 5 and 14 in r1, call 4 in r2), while the Service in front of those pods had no endpoints. A citation shows where a claim came from, not that the evidence supports it. K8sGPT, which scans everything instead of following the alert, saw the second fault in both runs, but its suggested fix was to label the pods by hand with tier=web rather than revert the selector.
The lesson for the method: an investigation is scoped by its alert. "Everything else is healthy" needs a request-path check (Service endpoints, the load generator's or users' errors), not pod status. The runbook for a second fault the alert does not cover is built on these runs, and the Service with no endpoints runbook covers the selector fix.
What happens when the agent's command is blocked?
Both agents run behind a command guard, and both guards blocked reads the agents wanted. All three tools had the same admin kubeconfig in the lab, each behind its own guard.
| Blocked command | Tool | Runs affected | What happened next |
|---|---|---|---|
kubectl rollout history | Aurora (observability_only policy) | 26 of 38 | Read ReplicaSets and the Deployment instead (as in scenario 12); listed rollout history under "Not Checked" |
kubectl exec <pod> -- cat /etc/nginx/conf.d/default.conf | Aurora | scenario 19, both runs | r1 left the cause "not fully reconciled" and was graded partial; r2 was graded correct |
kubectl exec <pod> -- cat /etc/resolv.conf | Aurora | scenario 16 r1 | Graded correct |
kubectl rollout history | HolmesGPT (bash allow-list) | 12 of 38 | Read ReplicaSets instead (as in scenario 01 r1); 2 of the 12 were among its 7 non-correct runs |
kubectl rollout status | HolmesGPT | scenario 14 r1 | Graded correct |
kubectl exec ... -- cat /etc/resolv.conf and a DNS lookup | HolmesGPT | scenario 16 r2 | Queried ReplicaSets and the cluster DNS config instead; graded correct |
a describe ... | sed -n ... pipe | HolmesGPT | scenario 13 r2 | Ran other calls; graded correct |
Aurora's denial reads "Command blocked by organization policy: Mutating kubectl operations". The default policy's deny rule matches the whole rollout verb, so the read-only rollout history and rollout status go down with rollout restart and rollout undo. Our lab fix branch allowed the two read-only forms, and in the 6 runs on that branch rollout history ran 14 times and was never denied. As of 21 October 2026 the default policy on Aurora's main branch still denies the whole verb, and exec with it. HolmesGPT's bash toolset matches commands by prefix against an allow-list; per its docs, the default list includes kubectl get, describe, logs and top but not rollout or exec, and anything else needs approval. Our runs piped the prompt in ("Interactive mode disabled when reading piped input"), so nobody was there to approve and the command did not run.
A blocked read is a hole in the evidence. What separates a usable tool is whether the report says so. Aurora's reports did, in their "Not Checked" lists, though in one earlier run it repeated the policy's wording and called rollout history "a mutating operation" in the report. The scenario 19 miss also shows the cost of a missing fallback: Aurora's own get pods output showed the failing pod was 9 seconds younger than the healthy one, which was enough to see it had loaded the edited ConfigMap. The ConfigMap change not applied to all pods runbook walks through that check.
The trade-off is real: loosening a guard so the agent can run exec also lets it run anything inside your containers. We would allow rollout history and rollout status and keep exec blocked, and accept that some faults then need a person.
Can an AI agent investigate a security incident?
Not the tools and faults on this page. Everything above is operational: a service is failing and the agent finds the change that broke it. A security incident investigation (a suspicious login, a malware alert, data leaving the network) runs on different data, such as SIEM events, endpoint telemetry and identity logs, and is the job of AI security operations (SOC) tools. We have not tested any of those, and the lab has no security scenarios, so we make no claims about them.
Two things do carry over. The loop is the same shape: a request comes in, the agent queries, cites evidence and concludes. And so does the audit question security teams ask about any agent: what exactly did it run? In the lab, Aurora's trace records every command with its full output, including the denied ones (POLICY_DENIED with the command text), and Aurora screens each command against hand-written rules plus a vendored subset of SigmaHQ detection rules before it runs. HolmesGPT prints every tool call and every blocked command in its output. If you are evaluating an investigation agent for your security team's review, ask for that record from a real run, not a description of it.
What should you require from any AI investigation tool?
From the 76 Aurora and HolmesGPT runs in the lab, the requirements that would have caught the problems we saw:
- Every tool call and its output, in order. You cannot judge an answer without the trace. Both traced agents provide one.
- Claims tied to specific evidence, and a habit of opening that evidence. Aurora's scenario 12 reports cited real outputs for a false claim.
- A "ruled out" and a "not checked" list. Not checked is where blocked commands and skipped areas belong.
- A check beyond the alerted object before anything is declared healthy: endpoints, traffic, the users' side of the request path.
- A read-only identity with a guard you can read, and a record of what the guard blocked. Decide in advance which blind spots you accept (
exec, Secrets, rollout history). - Fixes as suggestions with a risk rating and an undo, run only after a person approves. Aurora's scenario 13 suggestion had the rating and no undo.
- Isolation between trial runs. Aurora keeps memory and an incident index per organization. In our first, superseded batch, all runs shared one organization and Aurora cited "prior incidents" from an earlier fault; after that every run got a fresh organization.
- Time and cost per run, including background jobs. In the lab, Aurora's per-cluster discovery job cost about twice what the 38 graded investigations did ($29.70 for 30 discovery runs against $14.53).
- Two runs per fault. HolmesGPT was wrong and then correct on scenario 19; Aurora was partial and then correct on the same fault.
The AI incident investigation tools list has the commands to run these faults against any product in a trial. For one broken pod, the Kubernetes pod diagnoser takes pasted kubectl output and returns the likely cause in your browser.
How accurate is AI incident investigation in our lab?
| All 19 scenarios, 2 runs each | Aurora | HolmesGPT | K8sGPT |
|---|---|---|---|
| Correct | 34 of 38 | 31 of 38 | not scored as a peer |
| Partial | 4 | 6 | 21 |
| Wrong | 0 | 1 | 5 |
| "No problems detected" | 0 | 0 | 12 runs (6 scenarios) |
| Median time per run | 153.5 s | 45 s | 12.5 s for two analyze passes |
| Median LLM cost per run | $0.35 | not captured | not captured |
A judge model (Claude Opus 5.5) graded every answer against the injected fault. A person at Arvo read all 38 Aurora grades and changed none. The 76 HolmesGPT and K8sGPT grades are judge-model only: a second model re-read all 76 and agreed with every one, and a third pass graded them from scratch, agreed on all 38 HolmesGPT verdicts and disputed 6 K8sGPT labels. A blinded regrade with the tool names removed matched the published verdict in 38 of 38 Aurora runs, 37 of 38 HolmesGPT runs and 36 of 38 K8sGPT runs. The two-sided Fisher exact test that /benchmark computes on these totals gives p = 0.52, comparing person-read Aurora grades with judge-only HolmesGPT grades, so read Aurora and HolmesGPT as roughly even on accuracy. HolmesGPT is about three times faster at the median. Aurora's runs are from 2 October 2026, the others from 15 and 16 October, on the same cluster setup (kind v0.33.0, Kubernetes v1.37.0, 3 nodes) and faults. Arvo wrote the scenarios, a kind cluster with one small app is not production, and two runs per fault do not measure variance. The lab report has the method and every miss, and agentic incident response on a real alert traces a third investigation (scenario 07) call by call.
Aurora is open source under Apache 2.0: github.com/Arvo-AI/aurora, or book a demo to see it on your own alerts.
Sources: run folders runs/v1-run1/13-networkpolicy-blocks-redis, runs/v1-run1/12-two-faults-oom-and-selector, runs/v1-run1/19-config-drift-partial-restart and runs/h2h-v1/r1|r2, plus benchmark/results.json, REPORT.md, FINDINGS.md, runs/h2h-v1/README.md, review-summary.md, review2-summary.md and runs/blind-regrade.md in Arvo-AI/aurora-lab. Aurora's default command policy read on its main branch and HolmesGPT's bash toolset docs read on 21 October 2026. Verified 21 October 2026.