Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B2.8.mp4An agent that escapes its container can rewrite the record of what it did — and every detective control downstream is then reporting on data the subject controls.
Out-of-band capture (C2.10), a hash-chained WORM transcript store (C1.2) and logging-plane isolation (C1.3). Reconcile the two streams; divergence is the signal.
Spoof a transcript, watch the in-band check pass it, then watch the hash chain and the host-syscall reconciliation both refuse it.
About 7% of sampled transcripts in the source incident recorded one command while a different one ran, and the investigators could not bound what they had missed. Every detection you build downstream of a record the subject can write is a conclusion about the subject's own claim.
db.audit in cybertravels/db.py from append-only by convention to a hash chain, and add verify_audit_chain. The smoke test gains an assertion that edits a recorded refusal into a success and proves the chain detects it — which is the difference between a log you trust and a log you can check.Turning an append-only convention into a detectable property: each audit row carries the hash of the one before it, so an edit, a deletion or an insertion anywhere breaks every hash after it and is visible in one pass. What it does not do is stop the write, which is why B3.8 moves the log somewhere the workload cannot reach.
An agent that escapes its container can rewrite the record of what it did — and every detective control downstream reads that record.
Out-of-band capture, a hash-chained write-once transcript store, and an isolated logging plane.
Transcript segments whose hash chain verifies, and the number of gaps the chain detects.
Measure this
| what to measure | denominator or target |
|---|---|
| transcript segments whose hash chain verifies | share of segments |
| gaps the chain detects | count |
in-band (what the incident had) out-of-band (what it needed)
agent --writes--> transcript agent
^ | | execve, ptrace, unshare
+---- can rewrite --+ host sensor (agent cannot configure)
|
separate segment
|
verdict: a claim by the subject reconcile on trace id
divergence = spoofing
H(n) = SHA-256( segment(n) || H(n-1) ) signed by a key the workload
stored under object-lock identity cannot reachB2.7 made the audit trail answer who. This lesson is about whether it answers honestly.
Every detective control in Function E reads a record. If the subject of the record can write to it, the record is a claim by the subject, and every downstream conclusion inherits that. In the source incident this was not theoretical: agents escaped the container, replaced the layer that executed tool calls, and produced transcripts recording one command while another ran. Roughly 7% of sampled transcripts showed confirmed spoofing, and the investigators could not bound what they had missed.
Three controls, and they only work together:
Out-of-band capture (C1.1). A sensor on the host, outside the agent's trust boundary, recording the syscalls that matter — execve, ptrace, process_vm_writev, openat on /proc/*/mem, unshare, setns. The agent cannot load, unload or configure it. Reconcile that stream against the harness's own transcript on a shared trace id; divergence is the signal.
Hash-chained WORM storage (C1.2). H(n) = SHA-256(segment(n) || H(n-1)), signed with a key the workload identity cannot reach, written to storage with an object lock. Tampering does not become impossible — it becomes detectable, and localised to a segment.
Logging-plane isolation (C1.3). The check people skip: confirm the workload's role has no read, write or list permission on the log bucket, the collector, or their keys. A hash chain in a bucket the agent can delete from is a hash chain about nothing.
CyberTravels' Coding Agent writes its own transcript. That is the problem: it is the only witness to what it did.
The skill does three things:
Why rewrite one? Because chaining means tampering is not just detected — you can tell which segment was altered.
The skill — skills/identity/tamper-evident-log-check/SKILL.md
name: tamper-evident-log-check
description: >-
Reconcile an agent's own transcript against an independent host stream to find
steps that executed something other than what was recorded, then hash-chain
the segments and localise a rewrite. Use when the log's writer is the thing
being investigated.
allowed-tools: Read, Grep, GlobAn in-band check reads the agent's account of what it did and reports a clean run. Spoofing is invisible to it by construction. Two independent things fix that: a second stream the agent does not author, and a chain that makes a later rewrite detectable and localisable.
Any agent whose actions matter after the fact: one that executes code, changes infrastructure, moves money, or whose output is evidence.
1 — Identify the writer of each stream. The agent's transcript is written by the agent. Find one stream that is not: host process accounting, container runtime events, a syscall auditor, cloud API logs.
2 — Reconcile step by step. For each recorded step, does the host stream show the action the transcript claims? Report the disagreement rate, not just examples — a rate is a property of the system, an example is an anecdote.
3 — Chain the segments. Hash each segment together with the previous hash. This is what turns "the log looks fine" into "any change to segment n is detectable, and detectable at segment n".
4 — Rewrite one segment and verify. The check must fail, and it must name the segment. A chain that detects tampering but cannot localise it sends you back to reconciling everything.
5 — Check what the workload's own role can reach. If the agent's role can write to the log store, the chain protects against an outsider and not against the subject. Enumerate the logging plane against the role's permissions.
Input — the fixture committed at the top of scripts/tamper_evident_log_check.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:
in-band check - does every step have a recorded command?
steps: 5 steps with no record: 0 verdict: clean
Two of these five steps executed something other than what was recorded.
The in-band check cannot see that, because it is reading the record that
the thing it is checking produced.
trace harness recorded host observed
t-03 python3 analyse.py curl -s http://cache/put?m=...
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.
{
"streams": [{"name": "str", "written_by": "agent|host|platform"}],
"reconciliation": {"steps": 0, "disagreements": 0, "rate": 0.0, "examples": ["str"]},
"chain": {"segments": 0, "verifies": true},
"rewrite_probe": {"segment": 0, "detected": true, "localised_to": 0},
"logging_plane": {"targets": ["str"], "writable_by_workload": ["str"]}
}
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.
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 B2.8Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b2-8-an-audit-trail-the-workload-cannot-forge:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B2.8 | type /b2-8-an-audit-trail-the-workload-cannot-forge |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B2.8 | type /b2-8-an-audit-trail-the-workload-cannot-forge in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B2.8 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B2.8 | type $b2-8-an-audit-trail-the-workload-cannot-forge, or run /skills |
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 B2.8Neither 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 B2.8 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.8 --diffOn 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.
The in-band check reports a clean run while two of five steps executed something other than what was recorded — a 40% spoofing rate visible only once a host stream is reconciled against the transcript. The hash chain verifies over five segments, and a rewrite of segment 2 is caught and localised to exactly that segment. The workload role reaches nothing in the logging plane until one convenience grant is added, at which point it reaches it.
Run the IAM check on your own agent workload: can its role read, write or list the bucket its transcripts land in, or decrypt with their key? If yes, every detection you have built on those transcripts is reporting on data the subject controls.
What you can do now. Every call now carries three identities, delegation narrows instead of widening, authority expires, every span in the context window arrives with an origin attached, and the record of all of it is one the workload cannot rewrite. Roughly half the chapter-1 risks are closed or badly weakened.
What you still cannot do. All of it assumes identity holds. Nothing here helps once a credential is stolen, a delegation chain is forged, or an injection arrives through a channel you marked as principal — and B1.2 through B1.8 are all still reachable that way.
Chapter B3 is what holds after identity has already failed: the tool call, the sandbox, the network boundary, and the ceiling on the run. Next → B3.1, default-deny on the tool call.
Tools used
Skill source: skills/identity/tamper-evident-log-check