All lessons › Function E — The Agentic SOC › Recover and Root Cause — From Stopped to Back at Target
E5.2

The root cause record — naming a control, not a person

E5
▶
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/E5.2.mp4
Risk

The incident closes with a narrative, so the same control gap produces the same incident two quarters later.

Control

A structured root cause record naming the failed control, the detection that should have fired, and the specific change proposed.

The lab

Build a root cause record from a reconstructed incident and check it names a control rather than a person.

◉Use case relevance

The postmortem says the on-call engineer missed the alert. It is true, it is useless, and it will be true again next quarter about somebody else. A root cause names a control, or it names nothing that can be built.

At CyberTravels. RootCause refuses "human error", "a process gap" and "insufficient training" outright, and requires all three fields: the control that failed, the detection that should have fired, and the specific change. Name the thing that would have stopped it whoever was on shift.

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

A root cause record that names a control rather than a person or a narrative, with all three fields required — what failed, what should have detected it, and the specific change proposed.

Day 0 — why

An incident that closes with a narrative recurs, because nothing in a narrative can be built or measured.

Day 1 — how

Walk the control chain, mark each present, absent or present-but-wrong, and test that the statement names a control.

Day 2 — measure

A mechanical test: does the root cause name a control? "The engineer missed the alert" is true and fails it.

Measure this

what to measuredenominator or target
root causes that name a controlshare; "the engineer missed the alert" is true and fails the test

▦The framework, and how it works

   control chain for INC-2026-114        status

   B2.6  provenance at ingress           absent   <- first absent = ROOT
   B3.1  default-deny on the tool call   wrong scope
   E2.2  detection: refund w/o approval  absent   <- contributing
   E1.2  drift on vendor tool descs      absent   <- contributing
   E4.4  stop authority in the window    PRESENT  <- and never reached

   a control that exists but is never reached is not a mitigating factor.
   it is evidence about the detection in front of it.

   statement test:  names a control -> usable
                    names a person  -> rejected, however true

"The on-call engineer missed the alert" is true and useless — it will happen again next quarter to a different engineer. "We should have been more careful" names no control and no change anyone can make.

A usable root cause names the control that should have caught this and did not, in a form the next change can act on and the KCIs can later measure. The test is mechanical, and it should be: does the statement name a control?

Anchor → E1.0. A root cause naming a person ends no interval, because nothing in it can be built or measured. Naming the control that should have caught this gives the next two lessons something to change and something to re-read.

2 · Present, absent, or present-but-wrong

Three categories, and the third is the one that gets missed.

In the CyberTravels incident, stop authority was present and the incident still ran 194 minutes. A control that exists but is never reached is not a mitigating factor — it is evidence that the detection in front of it was the gap, which is a different finding and a different fix.

3 · Three candidate statements, one usable

The rejections are the lesson. Both rejected statements are true; neither can be built, tested or measured.

▶Real time execution as skill

The skill — skills/response/root-cause-record/SKILL.md

name: root-cause-record
description: >-
  Turn a reconstructed incident into a root cause record that names the control
  that failed, and reject statements that name a person or an intention. Use when
  closing an incident, when a postmortem produces a narrative instead of a
  change, or when the same incident class keeps recurring.
allowed-tools: Read, Grep, Glob

A root cause is a control, not a story and not a person

"The on-call engineer missed the alert" is true and useless: it will happen again next quarter to a different engineer. "We should have been more careful" names no control and no change.

A usable root cause names the control that should have caught this and did not, in a form the next change can act on and kci-fix-validation can later measure.

When to use this

At incident close, before the postmortem is written, and in review of any incident whose remediation was "increased awareness" or "additional training".

Step-by-step

1 — List every control in the chain, not just the one that failed last. Detection, prevention, authority to stop.

2 — Mark each present, absent, or present-but-wrong. The third category is the one that gets missed.

3 — The first absent control in the chain is the root cause candidate. Later absences are contributing.

4 — Test the statement: does it name a control? If it names a person, a team, or a state of mind, it is rejected. This is mechanical and should be.

5 — Carry the named control forward as a KCI. That is the handover to E5.4 and to the policy change proposal.

Example

Input — a five-control chain and three candidate statements, in scripts/root_cause_record.py.

Output — a real run:

   REJECT  the on-call engineer missed the alert
           -> names no control
   ACCEPT  no control compared the vendor tool description against the version approved at onboarding

Output contract

{
  "incident": "str",
  "root_control": "str",
  "contributing": ["str"],
  "statements": [{"text": "str", "accepted": true}]
}

Common edge cases

  • The control existed and was never reached. That is evidence about the detection in front of it, not a mitigating factor.
  • Several controls absent at once. The first in the chain is root; naming all of them as root produces a list nobody actions.
  • A genuine human error with no control behind it. Then the finding is that no control existed — which is a control statement.

Failure modes

  • Narrative postmortems. Readable, and they change nothing.
  • Blame, however gently worded. It ends the analysis one step early.
  • A root cause with no KCI. Nothing will ever check that the fix worked.

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 E5.2

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

Then pick e5-2-the-root-cause-record:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons E5.2type /e5-2-the-root-cause-record
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons E5.2type /e5-2-the-root-cause-record in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons E5.2type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons E5.2type $e5-2-the-root-cause-record, 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 E5.2

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 E5.2 --out work/cybertravels
python3 scripts/checkpoint.py --at E5.2 --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

One of three statements accepted. The first absent control in the chain is the root cause; the later absences are contributing.

✎Your turn

Take your last postmortem's root cause and run the test on it. If it names a person or an intention, rewrite it as a control.

Skill source: skills/response/root-cause-record

← PreviousE5.1 · Replay and forensics — reproducing a run you can defend