All lessons › Function E — The Agentic SOC › Understand — Correlation, Intel and the Hunt
E3.3

The context that makes agent triage work

E3
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/E3.3.mp4
Risk

Generic triage agents underperform your worst analyst.

Control

Feed the baseline, known FPs, crown-jewel map and prior decisions.

The lab

A/B a generic prompt vs a context-loaded one on the same alert set.

◉Use case relevance

Most bad triage is not a bad model. It is an agent asked to decide without the identity, asset and history context a human analyst would have pulled without noticing they pulled it.

At CyberTravels. The context that makes agent triage work is the context your own tree already holds: sensors.matrix() for what is observable, LABELS.md for the known defects, E1.2's baseline for what normal looks like this week, and the prior decisions in the audit log. A triage agent given none of it underperforms your worst analyst.

◴What this lesson is — Day 0, Day 1, Day 2

What a triage agent has to be given before it beats a junior analyst: the baseline, the known false positives, the crown-jewel map and the prior decisions — all of which the system already holds.

Day 0 — why

An alert about an agent without scope, identity and delegation is not triageable, so analysts escalate everything or quietly close everything.

Day 1 — how

Add the three fields an agent alert needs on top of who, what and when.

Day 2 — measure

Analyst decisions matching ground truth with and without context — the same alert, two accuracies.

Measure this

what to measuredenominator or target
analyst decisions matching ground truth, with and without contexttwo accuracies on the same alert

▦The framework, and how it works

   the alert                what a human would have pulled without thinking
   +----------------+       +-----------------------------------+
   | user: dana     |  -->  | is dana on call?                  |
   | host: build-07 |       | is build-07 a build agent?        |
   | 03:14          |       | has this fired for dana before?   |
   +----------------+       +-----------------------------------+

   most bad triage is missing context, not a weak model

An alert about a human is triageable with three facts: who, what, when. An alert about an agent needs three more, and without them every analyst has to guess.

  • The acting identity and the principal it acted for (B2.1).
  • The scopes it held at the time. This is the decisive field: reading .env is alarming for an agent scoped repo:read and routine for a secrets-rotation agent.
  • The delegation chain, so the analyst can see who caused the task.

Without scope in the alert, the analyst's only options are to escalate everything or to develop a habit of closing agent alerts. Both happen, and the second one happens quietly.

Anchor → E1.0. Acting identity, scopes held and the delegation chain are what let the interval end at all. Without them triage does not take longer; it stops being triage, because reading .env is alarming for one agent and routine for another and nothing on the alert says which.

2 · Demo — the same alert, with and without context

3 · Where it breaks — measure the analyst's decision quality

4 · The control — the six fields, and what each one decides

5 · Triage as a skill — and the sample that keeps it honest

Context turns a guess into a verdict. Automating the verdict without automating the audit of it is how a closing rule quietly starts closing real incidents.

The skill therefore requires a sampling rule over anything auto-closed, and requires its seed to come from something stable. Sampling seeded from hash() picks a different subset on every run, so you can never tell whether a change in findings came from the rule or from the dice.

▶Real time execution as skill

The skill — skills/secops/detection-triage/SKILL.md

name: detection-triage
description: >-
  Triage security alerts with the context needed to reach a defensible verdict,
  and sample what is auto-closed so the closing rule stays honest. Use when
  working an alert queue, deciding whether an alert is a true positive, tuning
  a noisy detection, or designing automated alert handling.
allowed-tools: Read, Grep, Bash

Alert triage with context

An analyst reading an alert in isolation is guessing. The verdict comes from the alert plus the context that makes it normal or abnormal, and most triage automation fails because it automated the guess instead of the context.

When to use this

Working an alert queue, building an auto-close rule, or reviewing why a detection produces verdicts nobody trusts.

Procedure

1 — Gather the context before judging. For every alert, assemble:

  • asset — what it is, who owns it, how exposed it is
  • identity — human or workload, and its normal behaviour
  • history — has this fired before on this asset, and how was it resolved
  • change — was there a deploy, a migration, a new agent, in the window
  • peers — did the same thing fire elsewhere at the same time

A verdict issued without history will re-litigate a decision the team already made, which is the most common way triage automation loses trust.

2 — Reach a verdict, with the reason. One of true_positive, false_positive, benign_true_positive (it really happened and it is fine), or needs_human. Record which context field decided it. "Benign true positive" is a distinct category and collapsing it into false-positive corrupts every tuning decision made from the data afterwards.

3 — Attach confidence, and let it gate automation. Only high-confidence verdicts may auto-close. Everything else queues.

4 — Sample the auto-closed. Automation that closes alerts must be audited by re-opening a fraction of them for human review. This is the control that catches a closing rule that has quietly started closing real incidents.

Seed the sampler from something stable — a checksum of the alert id, never hash(), which Python randomises per process. A sampling rule that picks a different subset every run cannot be audited, because you cannot tell whether a change in findings came from the rule or from the dice.

5 — Feed tuning from verdicts, not volume. A detection is noisy if its false-positive rate is high, not if it fires often. Rank tuning candidates by false_positive_rate × volume, with a full tiebreak so the list is stable between runs.

Example

Input — the fixture committed at the top of scripts/detection_triage.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

BARE ALERT (what most SOCs receive):
   patch-agent read /vault/.env
   rotator-agent read /vault/.env
   → identical. An analyst cannot tell these apart.

ENRICHED ALERT:
   actor        patch-agent
   on behalf of dana@corp

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "triaged": [
    {"alert_id": "str", "verdict": "true_positive|false_positive|benign_true_positive|needs_human",
     "deciding_context": "asset|identity|history|change|peers",
     "reason": "str", "confidence": 0.0,
     "auto_closed": false, "sampled_for_review": false}
  ],
  "sampling": {"rate": 0.0, "seed_source": "str", "reviewed": 0, "disagreements": 0},
  "tuning": [{"rule": "str", "fp_rate": 0.0, "volume": 0, "priority": 0.0}]
}

disagreements is the number that matters: it is the measured error rate of the automation, and it belongs in every report about it.

Failure modes

  • Auto-closing without sampling. The rule then has no error bar and no way to acquire one.
  • Seeding the sampler from hash(). Non-reproducible sampling is not a control.
  • Folding benign-true-positive into false-positive. It teaches the tuner to suppress a working detection.
  • Removing needs_human. Forced verdicts under uncertainty are how a queue becomes an incident.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons E3.3

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick e3-3-the-context-that-makes-agent-triage-work:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons E3.3type /e3-3-the-context-that-makes-agent-triage-work
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons E3.3type /e3-3-the-context-that-makes-agent-triage-work in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons E3.3type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons E3.3type $e3-3-the-context-that-makes-agent-triage-work, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py E3.3

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at E3.3 --out work/cybertravels
python3 scripts/checkpoint.py --at E3.3 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The bare alert is identical for both agents. Enriched, the secrets-rotation agent is within remit and the patch agent is not. Context-free triage escalates both — generating a nightly false positive — while scope-aware triage matches ground truth on both.

✎Your turn

Check which of the six fields your agent telemetry carries today. Scopes-held is the one almost nobody logs, and it is the one that decides the alert.

Tools used

WazuhGLM-4.6Llama 3.3Claude Haiku 4.5

Skill source: skills/secops/detection-triage

← PreviousE3.2 · Admission rules — what the investigating agent may touch