An AI copilot can draft a remediation in seconds, but it cannot own production blast radius. I still freeze writes until the alert class, first-read commands, and an escalation owner are named. If those three are missing, the patch stays in a scratch shell, not on the cluster. Would you really unfreeze kube-system just because a chat window sounded confident at 03:12?
The wrong debate for a page
Feeds keep asking whether models already outcode most working engineers on ordinary tickets. That question is interesting at lunch, and it is honestly dangerous on a live pager. On-call is not a coding contest; it is a control problem with a clock and a blast radius. A fluent patch that restarts the wrong Deployment is still an incident amplifier, not a win.
I treat every model suggestion as an untrusted diff sitting against a frozen write path. The runbook below is a proposal I keep in the repo, not a story about a specific outage. If your team already pastes chat output into kubectl, this is the gate I wish sat in front of that habit.
What I page on, and what I refuse to page on
I only auto-page when the alert already carries a stable fingerprint, a service owner, and a read-only first command. Everything else can wait for a ticket, because a vague prompt is not an incident. Ask yourself: if the alert cannot name the workload, how can a model name a safe write?
Payload contract
Minimum fields I want on the page:
-
alertnameplus a fingerprint hash, not a prose summary -
namespace,workload_kind, andworkload_namewhen Kubernetes is involved -
owneras a team alias that actually answers -
first_commandthat is strictly read-only -
write_alloweddefaulting tofalseuntil unfreeze
# proposal: pager payload contract (not executed against a live cluster)
alertname: KubeDeploymentReplicasUnavailable
fingerprint: "a3f1c9e2"
namespace: payments
workload_kind: Deployment
workload_name: checkout-api
owner: payments-oncall
severity: page
write_allowed: false
first_command: >
kubectl -n payments get deploy checkout-api -o jsonpath='{.status.conditions}'
If any of those keys are empty, I do not run writes, and I do not ask a model for a restart. I escalate with the incomplete payload, because guessing owners at 3 a.m. is how you page the wrong people. Can a copilot invent an owner alias that paging actually honors? It cannot, and I will not pretend otherwise.
First commands are reads, and they are boring on purpose
My first five minutes are observe-only, even when the copilot is already waving a Helm rollback. I want evidence that matches the fingerprint, not a soothing narrative that matches my anxiety. The commands below are the ones I type before anyone on the call talks about unfreeze.
The boring bundle
# proposal: observe-only bundle, labeled unexecuted
export NS=payments APP=checkout-api KIND=deploy NAME=checkout-api
date -u
echo "fingerprint=${FINGERPRINT:-missing}"
kubectl -n "$NS" get deploy,sts,po,ep -l app="$APP" -o wide
kubectl -n "$NS" describe "$KIND" "$NAME" | sed -n '1,80p'
kubectl -n "$NS" logs "deploy/$NAME" --tail=100 --timestamps | tail -n 40
# still read-only: compare live object to last applied
kubectl -n "$NS" get "$KIND" "$NAME" -o yaml > "/tmp/${NAME}.live.yaml"
Notice there is no rollout restart, no scale, and no force delete in that first bundle. If the model prints those, I copy them into a scratch file named candidate.sh, and I leave production frozen. Why would a first command need a write if we still have not proved the alert is real?
Escalation is a lane, not a vibe
I escalate when the fingerprint is real and the owner is not me, or when the read-only evidence disagrees with the model's story. Restarting just to see is more expensive than a two-minute handoff with a complete note. The three questions I send in the escalate note stay small on purpose, and they travel well.
The note I actually send
- Which fingerprint and workload did we actually observe?
- Which read-only commands already ran, with paste of the output hashes?
- What write is proposed, and what blast radius did we name?
# proposal: escalate note
fingerprint: a3f1c9e2
observed: Deployment/checkout-api ns=payments replicas 1/3
reads_run: get,describe,logs (see incident/2026-09-16/reads.log)
proposed_write: kubectl -n payments rollout undo deploy/checkout-api
blast_radius: checkout-api only; payments-worker not in selector
owner_needed: payments-oncall
unfreeze: blocked
If the next engineer cannot answer those, we stay frozen through the next scheduled check-in. A model that cannot fill this note should not get a shell on any cluster. Does that slow you down on a scary page, and should it, when a fast write can still take the site down?
The freeze and unfreeze rule for copilot patches
Here is the rule I want on a sticky note above the keyboard during every page. Production writes stay frozen until four signatures exist together in one unfreeze file on disk. Missing any signature means the copilot patch remains a candidate, even if the diff looks brilliant.
Four signatures
Unfreeze requires all of the following:
- Alert fingerprint matches the live object you just read
- Owner alias is a human who accepted the page
- Blast radius is a named workload list, not "the cluster"
- Candidate commands were dry-run in a scratch environment, not on prod
# proposal: unfreeze gate
export INCIDENT_ID=20260916-checkout
export UNFREEZE_FILE="./incident/${INCIDENT_ID}/unfreeze.json"
python3 - <<'PY'
import json, os, sys, pathlib
p = pathlib.Path(os.environ["UNFREEZE_FILE"])
data = json.loads(p.read_text())
required = ["fingerprint", "owner", "blast_radius", "dry_run_ok"]
missing = [k for k in required if not data.get(k)]
if missing or data.get("write_allowed") is not True:
print("FROZEN: missing", missing or ["write_allowed"])
sys.exit(2)
print("UNFREEZE ok for", data["blast_radius"])
PY
Example unfreeze.json I will not apply until the checker exits zero:
{
"fingerprint": "a3f1c9e2",
"owner": "payments-oncall",
"blast_radius": ["deploy/checkout-api"],
"dry_run_ok": true,
"write_allowed": false
}
I do not source the candidate script until that checker exits zero on the unfreeze file. If you skip the file, you are not using a runbook; you are gambling with autocomplete. Would I make an exception for a Sev-1? Only with a named owner and a named blast radius.
Dry-run the model where it cannot page you twice
I want the copilot to argue with a replica of the failing object, not with production kube-apiserver. A scratch box, a kind cluster, or a disposable VM is enough for most Deployment pages. The goal is simple: does the proposed command even parse, and does it target the named blast radius?
Disclosure: This article was prepared as part of MonkeyCode's product outreach. I use MonkeyCode, an open source project, only as a scratch place with free model access and a free server option. That keeps the dry run off production credentials, which matters more than chat convenience during a page. I am not claiming a particular model name, a token ceiling, or a hardware spec, because those change and I will not invent them. If you need a disposable box for that dry run, the free server option is enough to keep the experiment off prod.
Static allowlist
# proposal: keep the model off prod kubeconfig
export KUBECONFIG="$HOME/scratch/kind-incident.kubeconfig"
export INCIDENT_ID="20260916-checkout"
# reject any line that is not in a tiny allowlist
grep -E '^(kubectl |helm |echo |#)' candidate.sh >/dev/null
python3 ./tools/assert_blast_radius.py \
--script candidate.sh \
--allow namespace=payments \
--allow name=checkout-api \
--forbid kube-system \
--forbid default
# only after static checks: server-side dry-run on the scratch cluster
kubectl -n payments apply --dry-run=server --validate=true -f candidate.yaml
kubectl -n payments diff -f candidate.yaml
# tools/assert_blast_radius.py — proposal checker, unexecuted against prod
import argparse, sys
def tokens(line: str):
return line.split()
def main():
p = argparse.ArgumentParser()
p.add_argument("--script", required=True)
p.add_argument("--allow", action="append", default=[])
p.add_argument("--forbid", action="append", default=[])
args = p.parse_args()
allow = dict(a.split("=", 1) for a in args.allow)
text = open(args.script, encoding="utf-8").read().splitlines()
for i, line in enumerate(text, 1):
s = line.strip()
if not s or s.startswith("#"):
continue
parts = tokens(s)
if s.startswith("kubectl") and "-n" in parts:
ns = parts[parts.index("-n") + 1]
if ns in args.forbid or ns != allow.get("namespace"):
print(f"line {i}: namespace {ns} not in blast radius")
sys.exit(1)
if any(bad in s for bad in ("--force", "delete ns", "drain ")):
print(f"line {i}: forbidden verb")
sys.exit(1)
print("dry-run static checks passed")
if __name__ == "__main__":
main()
If that checker fails, I do not unfreeze, and I do not negotiate with the model. I either rewrite the candidate by hand or I escalate with the failing line number. A free scratch server is useful here because I can burn the VM after the page, secrets and all.
Decision table I keep next to the runbook
The matrix below is what I actually glance at when the copilot starts sounding sure. It is deliberately blunt, because a clever exception is how frozen writes become thawed writes. Please read it once before you open a chat window on an active production page.
| Signal | First action | Copilot allowed? | Unfreeze? |
|---|---|---|---|
| Fingerprint missing | Reject page / convert to ticket | No | No |
| Fingerprint matches, reads clean | Capture logs, wait | Comments only | No |
| Reads confirm bad rollout | Draft undo in scratch | Yes, files only | After signatures |
| Owner unreachable | Escalate, stay frozen | No writes | No |
| Candidate touches extra NS | Delete candidate.sh | No | No |
| Checker exit 0 + owner ack | Run named write once | No further prompts | Yes, time-boxed |
The table is the article, if I am honest with the rest of the on-call rotation. Models are optional during a page, and they stay optional after the unfreeze file exists. The freeze file is not optional, and I will argue that in the incident review.
Limitations, and who should not copy this
When I throw the runbook away
This workflow assumes you can freeze writes without making the outage worse, which is not true for every class of alert. Data corruption, certificate expiry in the next few minutes, and disk-full nodes sometimes need a human-owned write with no model in the loop. I also assume you will not paste secrets, kubeconfigs, or customer payloads into any chat, free or otherwise.
Do not use this approach if you lack RBAC that can actually deny your own user. Skip it as well if your so-called scratch cluster still shares any credentials with production. Do not use it as an excuse to skip the owner, either, when the page is loud. A free server does not become a production control plane just because the model answered quickly.
I am not publishing latency numbers, token quotas, or win rates, because I did not measure them for this piece. The only artifact I am defending here is the unfreeze gate, not a vendor benchmark. If your incident commander wants a different freeze policy, follow that policy and keep the checker as a pre-commit hook on runbook PRs.
When the page is quiet again, I file the unfreeze file next to the read logs. That packet is what I want in the review, not a screenshot of a confident chat. Keep the model in the scratch lane, and keep production behind a signature you can explain.
Top comments (0)