Chapter 01: Blast Radius & the Shape of Safety
Interactive Explainer
Video is being produced. Check back soon.
The intro asked one question: when someone makes a mistake - and someone will - how far can it spread? That reach is the blast radius: which services, environments, users and data one mistake can affect. This chapter makes the question concrete. It tells one story, takes apart the four mistakes in it, and shows which part of the platform stops each one - for a person and for an AI agent alike, because the cluster does not know who typed the command.
This chapter has almost no commands: the story shows a few, and the only thing you set up is a hook for your AI agent (section 5). The hands-on work starts in Chapter 02.
Learning Objectives
By the end of this chapter, you will be able to:
- Explain how one correct command, run against the wrong cluster, can rewrite a whole environment
- Name the four rules that apply to every change in this course, and say which rule a given change breaks
- Decide whether two changes belong in one pull request, using the single-rollback test
- Say which layer of the platform can stop a mistake, and which part of the course builds that layer
Recall Check
Before continuing, quickly recall:
- Why does this course treat AI as a very well-read junior engineer that proposes, rather than one that decides? (Intro)
- Which three commands prove that your lab cluster is green? (Chapter 00)
- Why does
age.agekeystay on your machine and never go into Git? (Chapter 00)
If you can’t answer these, revisit the intro and Chapter 00 before proceeding.
1. One Command, Wrong Cluster
Thursday, 15:00. An engineer has spent two weeks on a branch, feature/ingress-rework, that moves the frontend and the backend to a new ingress setup. Before opening a pull request, they want to see it running, so they do what they have done many times on their test cluster: point Flux at the branch.
kubectl -n flux-system patch gitrepository flux-system --type merge \
-p '{"spec":{"ref":{"name":"refs/heads/feature/ingress-rework"}}}'
The command is correct. The cluster is not. An hour earlier they had switched kubectl to production to check a log, and never switched back.
Pause and Predict: Production’s Flux now follows a two-week-old branch. Before reading on: what two different things will Flux do to production within the next minute? (Think about what is in the branch - and what is missing from it.)
Flux does exactly what it was told. Within a minute it fetches the branch and makes production match it:
- Everything the branch builds is applied - every folder the Kustomizations apply, now read from the branch. The new ingress setup - never reviewed, tested only on a laptop - is now what production traffic goes through.
- Everything missing from the branch is deleted. Flux runs with
prune: true: each Kustomization keeps an inventory of the objects it applied, and any of them that is no longer in what it builds gets removed. The branch was created two weeks ago. Since then,maingot a new worker Deployment and two NetworkPolicies. They are not in the branch, so Flux deletes them from production.
The engineer runs flux get kustomizations, sees every line Ready, and goes back to work.
Here is what went wrong, one mistake at a time.
The target was never checked. The command was right for the test cluster. Which cluster kubectl talks to is hidden state - the current context in the kubeconfig - and nothing checked it before the write.
Nobody saw what the change would do. On the test cluster the change was “try my branch”. On production it was “replace two weeks of other people’s work with my branch”. A diff would have shown three objects to delete and a new ingress setup. Nobody saw it, because there was no diff - just a command.
Everything changed at once. A normal release brings changes into main one pull request at a time, each with its own rollback. Pointing a cluster at a whole branch brings all of its changes at once - and takes away everything the branch does not have.
“Ready” was taken as proof. Ready on a Flux Kustomization means the reconciliation succeeded: the manifests were applied and, where the Kustomization waits for them, the applied objects passed their health checks (Flux). It describes the last reconciliation, not what Git says now: a suspended Kustomization keeps showing Ready while it ignores every new commit, so compare the revision it applied with the one you meant, and look at the SUSPENDED column (Chapter 03). And it does not mean the application works: the worker it had just deleted does not show up as an error, because as far as Flux knows, it is supposed to be gone.
Pointing Flux back at main recreates the deleted objects. It does not recreate what was lost with them: the jobs the worker did not process in the meantime, the traffic the missing NetworkPolicies let through, and - if a PersistentVolumeClaim is deleted, depending on its storage class - the data behind it.
The same command from an AI agent
Now the engineer asks their AI coding agent: “point Flux at my branch so I can test it.” The agent runs the same kubectl patch, against the same kubeconfig, in two seconds. It has no memory of the log check an hour ago, and it does not stop to wonder which cluster “the cluster” is. If it was also asked to try the branch “on all our clusters”, it does this on every one of them.
This is not hypothetical. In July 2025 an AI agent on Replit, working on a live project during an explicit code freeze, ran destructive commands and deleted the production database. It then said the data could not be recovered, which turned out to be false (The Register, AI Incident Database #1152). Replit’s response was to separate development from production, improve rollback, and add a mode in which the agent can plan but cannot change anything live - the same kind of protections this course builds.
Why “be careful” is not enough
The engineer in the story was careful. They ran a command they knew well, on purpose. Safety that depends on attention fails at 15:00 on a busy day, and fails more often as AI produces more changes than people can read. So the protections in this course are mechanical: checks that run whether or not anyone is paying attention. A red prompt for production is a useful hint, but it is still attention - it helps only if someone looks at it. A guardrail refuses the command even when nobody does.
2. The Four Rules
Each mistake in the story breaks one rule. The rules apply to every change in this course - Terraform, Kubernetes manifests, secrets, database migrations - and to every author: you, a teammate, or an AI agent.
I. Verify context before every write
Before a command changes anything, prove - with a check, not from memory - which cluster, namespace, Terraform state or branch it will change, and let the command name that target itself, so nothing can change it in between.
Why: kubectl, flux and terraform act on hidden state: the current context in your kubeconfig, the workspace you selected yesterday. In the story, one check - “is the current context the test cluster?” - would have caught it. A command that names its context (--context) does not depend on the current one at all: nobody can redirect it by switching the current context. It still trusts that the named context points to the right cluster - which is why the guard checks that the context exists before it runs the command.
A check alone is not always enough, because the current context is shared: it is one line in the kubeconfig file (by default ~/.kube/config) that every terminal using that file reads. Picture a careful engineer. In one terminal they check the context - test cluster, good. In a second terminal their AI agent is reviewing the clusters and runs kubectl config use-context production to read something there. The engineer’s next command, kubectl apply, goes to production. The check was true when it was made; the target changed between the check and the write, and neither of them did anything wrong on their own. The way out is to stop depending on the shared line: a command that writes names its cluster (kubectl --context test-cluster apply ...), and nobody - person or agent - switches the current context.
Built in: guard-kube-context.sh checks that the context and namespace exist, then runs the command with both pinned to it, so nothing can switch the target in between (Checkpoint A); a hook that stops an AI agent from switching the current context or running kubectl without --context (section 5); and an AI agent in the cluster that can read it but cannot change a workload - it writes nothing to the cluster at all, Events included; its findings go to Slack and its own API (Chapter 14).
II. Plan before apply
A change reaches a system only after someone has seen exactly what it will do, and what gets applied is exactly what they saw.
Why: in the story the engineer knew what the command was, not what it would do on that cluster. The plan is the part that shows the second.
Built in: for the cluster, the plan is the pull request into main - its diff shows what you meant to change; flux diff against the cluster shows what Flux would actually do, deletions included; CI builds and validates the manifests; and Flux applies only what is merged (Chapter 03). For infrastructure, it is a saved Terraform plan file, applied only while it is recent. The age limit narrows the window in which the infrastructure can change underneath the plan; it does not detect a change made by hand after planning (Chapter 02).
III. One failure domain per change
A failure domain is a part of the system that can fail on its own - one service, one environment, one schema change. Blast radius is how far a failure spreads; failure domain is about where it starts and what you have to undo. Changes that can fail independently go into separate changes. Changes that cannot work without each other go together.
The test for every pull request is:
“If this breaks, is there a single clean rollback?”
Why: in the story the new ingress setup, the deleted worker and the deleted NetworkPolicies arrived together. When something breaks, you do not know which of them to look at - and undoing one means undoing all of them.
The rule is not “one file per pull request”. Some changes cannot work apart, and splitting them causes the breakage: a feature flag ships with the code it gates; an API change ships with the callers that use it; a migration that adds a column ships with the code that reads it. One pull request is not one atomic step, though: the migration has to run before the new code starts, and the old code keeps working while it runs - as long as the migration stays compatible with it. A column the old code does not write must allow it to be missing: a required column is added as nullable (or with a default), filled in, and made NOT NULL only once no old writer is left. A migration that renames or drops a column is different - the old code still reads the old column, so it is split into separate releases: first expand (add the new column, write both), then, once no running code reads the old one, contract (remove it) - Chapter 18.
Built in: changes reach production one pull request at a time, through review and CI (Chapters 03 and 05).
IV. Evidence before merge and after deploy
A change is done when something other than its author shows that it works - at two moments:
- Before merge: the plan or diff, the CI run, the policy checks - what the reviewer approves.
- After deploy: the cluster runs the revision you merged, and the application’s own signals say it works - requests succeed, queues drain, nothing regressed.
A green CI run closes the first; only the second closes the change.
Why: Ready proved that Flux reconciled the branch, not that the application works. Evidence here would have been the application’s own signals: requests succeeding through the new ingress, the worker’s queue draining.
Built in: CI gates before merge (Chapter 05), and metrics and alerts after deploy (Chapter 11).
The four rules as four questions
Before any change - yours, a teammate’s or an AI agent’s - answer these. If you cannot, the change is not ready.
| Rule | Question | What you write down |
|---|---|---|
| I | Where will it act? | Cluster, namespace or Terraform state, environment, Git revision |
| II | What will it change? | What is added, changed and deleted - from the plan or diff, not from memory |
| III | How do I undo it? | The single rollback, or the recovery when a rollback is not enough (data, leaked secrets) |
| IV | What proves it works? | The checks before merge, and the signals you watch after deploy |
3. Where Each Mistake Can Be Stopped
The engineer’s command went straight from a terminal to the cluster. It skipped every check that lives around Git: hooks on the workstation, review, CI. That is why the platform is built in layers. A normal change passes through all of them. A shortcut - kubectl from a laptop, an agent with a kubeconfig - skips the early ones, and the later layers are all that is left.
The engineer in the story took the dashed arrow - kubectl straight to the cluster. Three layers never saw the command.
Not every layer catches every mistake. The table is honest about that.
| Layer | When it acts | In the story | Chapters |
|---|---|---|---|
| Workstation | before a command runs or a commit is made | the guard runs a write only against the context and namespace the command names, and pins it there - the patch could not have gone to whatever the current context was | 05, Checkpoint A |
| Review | before merge | the branch reaches production only by merging into main, which keeps the worker and the NetworkPolicies instead of deleting them | 02, 03, 05 |
| Delivery | from merge to cluster | production follows main, never a branch; images come only from CI | 03, 04, 05, 10, 17 |
| Admission | when the cluster receives an object | would not have helped here: the objects were valid, just the wrong ones. An enforced Kyverno policy rejects workloads that break it - unsigned images, pods without limits; the platform’s Kyverno policy packs refuse such workloads in develop, staging and production and only report them elsewhere (Chapters 16 and 17). One policy is different: the ValidatingAdmissionPolicy oidc-flux-operator-fields is Deny from the start | 16, 17 |
| Runtime | while the workload runs | limits what a bad change can do - but the NetworkPolicies live in Git, so the prune deleted them too | 06-09 |
| Operations | after something goes wrong | an alert on the stalled queue; a backup to restore lost data; a practised incident response | 11-15 |
Two things follow from this table.
No single layer is enough. Every layer can be skipped, misconfigured or simply not built for this kind of mistake.
The strongest protection is removing the shortcut - and making the safe path the easy one. Workloads in production change only through Git and CI (Chapters 03 and 05). In the story, the everyday kubeconfig could write to production. On the Hetzner cluster the everyday kubeconfig is the OIDC login, and it - with any AI agent running on it - can only read production. In flux-system, RBAC lets admins patch only Kustomizations and HelmReleases - GitRepositories stay read-only, so nobody can point Flux at a branch, and the Kubernetes API rejects the kubectl patch from the story. A separate admission policy, the ValidatingAdmissionPolicy oidc-flux-operator-fields (Deny from the start), narrows even those patches to suspend, resume and reconcile. The admin kubeconfig that Terraform writes can still change the GitRepository’s branch - it can change anything - and that is why it is kept for break-glass, an exception used by the on-call engineer in an incident, not for daily work. The in-cluster AI agent is no way around this either: it can read Kubernetes resources but not change them, and reports what it finds - to Slack, where one is configured (Chapter 14). Then the command in the story has nowhere to run with everyday access: the routine way to get a change into production is a pull request, and everything in Section 2 applies to it. The engineer who wants to try a branch gets a better place to do it than production: their own local cluster (Chapter 00).
For whatever still gets through, the platform itself limits how far it spreads. That is what most of the course builds, layer by layer.
4. What Each Chapter Gives You
The Curriculum lists every chapter: what it covers, its level, the time it takes and what it builds on. You do not need to remember chapter numbers - when a later chapter introduces a new tool, ask which layer from the table above it belongs to and which problem it removes. The advanced chapters at the end, 16-23, are published.
5. Instructions for Your AI Agent
Chapter 01 starts a list of rules for the AI agent you work with; each chapter adds its own as it is reworked. Put them into your agent’s instruction file (for example AGENTS.md or CLAUDE.md in your fork). An instruction is still attention - the agent follows it until it forgets, or until a new session does not know it - so each rule says what enforces it.
- Never change the current kubectl context (
kubectl config use-context,set-context, …). Enforced by: a hook that blocks it (scripts/agent-hooks/kube-context-hook.sh). - Every
kubectlandfluxcommand that talks to a cluster names it with--context. Enforced by: the same hook - it blocks such a command without it. Commands that only read your own kubeconfig or client version (kubectl config get-contexts,kubectl version --client) need no cluster and pass. - Writes go through the guard, which pins the target in the command itself:
scripts/guard-kube-context.sh --context <name> --namespace <namespace> -- <command>. Enforced by: the guard, once it is used - it adds the context and namespace to the command and refuses a second target inside it. Using it at all is an instruction: the hook only requires a named cluster, not the guard. - Try a branch on your local cluster; never point a shared cluster at a branch. Enforced by: on the Hetzner cluster, RBAC - the everyday sign-in cannot change where Flux reads from. On kind: instruction only.
Readyis not proof: report the application’s own signals - requests that succeed, queues that drain. Enforced by: nothing - instruction only.
A write through the guard (rule 3) - app.yaml stands for any manifest:
scripts/guard-kube-context.sh \
--context kind-sre-control-plane --namespace develop \
-- kubectl apply -f app.yaml
Connecting the hook. Once the hook runs before every command the agent starts, rules 1 and 2 hold even when the agent forgets them - for the commands the hook can read (the limits are below). It needs jq - make check-tools (Chapter 00) checks it. For Claude Code, put this into .claude/settings.json at the root of your fork. If that file already exists, add the PreToolUse entry to its hooks instead of replacing the file. The path scripts/agent-hooks/... is relative, so start the agent from the root of your fork:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' | scripts/agent-hooks/kube-context-hook.sh"
}
]
}
]
}
}
What it does: before every shell command (PreToolUse, tool Bash), Claude Code hands the planned call to this command as JSON. jq takes out the command text - for example kubectl apply -f app.yaml - and the hook checks it. Exit code 0 lets the command run; exit code 2 stops it and shows the agent the reason, such as does not name its cluster (–context), so it can correct the command itself. Other agents: pipe the command text to the same script from their own pre-command hook.
Prove it blocks. From the root of your fork, feed the hook the same JSON Claude Code sends, once without a cluster name and once with one:
echo '{"tool_input":{"command":"kubectl get pods"}}' | jq -r '.tool_input.command' | scripts/agent-hooks/kube-context-hook.sh; echo "exit=$?"
echo '{"tool_input":{"command":"kubectl --context kind-sre-control-plane get pods"}}' | jq -r '.tool_input.command' | scripts/agent-hooks/kube-context-hook.sh; echo "exit=$?"
Expected: the first prints [kube-context-hook] blocked: 'kubectl get pods' does not name its cluster (--context), then a [kube-context-hook] rule: ... line saying what to do instead, and exit=2; the second prints only exit=0. Nothing reaches a cluster - the hook only reads the text.
The hook reads the command as text. It checks kubectl and flux when the agent runs them directly, also in a chain like cd infra && kubectl ...; it does not see them inside a script or a bash -c "...". It is a seatbelt against the common mistake, not a lock - the lock is RBAC (rule 4).
Other agents. The hook is not specific to Claude Code: it reads one command as text on stdin and answers with its exit code. An agent with its own pre-command hook connects the same script - give it the command text (... | scripts/agent-hooks/kube-context-hook.sh) and treat exit code 2 as “do not run”. An agent without such a hook has nothing that stops it: the rules go into its instruction file (AGENTS.md, its rules file), and you run cluster commands through scripts/guard-kube-context.sh, which pins the context itself. That is an instruction the agent may forget, not a guard - say so to yourself before you trust it.
The full list, chapter by chapter, is docs/agent-rules.md in your fork of the platform repository (Chapter 00) - also on GitHub.
6. What You Need Before Chapter 02
Chapter 02 is where you stop reading and start running commands. Chapter 00 builds the environment every later chapter assumes: your fork of the platform repository, a local kind cluster reconciled by Flux, your own SOPS key, and the three commands that prove the cluster is green. Do it once, before Chapter 02.
You do not need a cloud account, a domain, or a managed Kubernetes cluster. The core track runs entirely on the local cluster; steps that need a cloud provider are marked Cloud track and can be read instead of executed.
Knowledge Check
Before starting Chapter 02, complete the Quiz to check that you can apply the four rules and place a mistake in the right layer.