The Banner Is Not a Lock: A Shadow Session Wrote the Row It Was Told Not To

· 6 min read · by Michael Kaminski

My LinkedIn engine runs every job in shadow mode before it is allowed to publish. On 2026-09-22 a shadow scorecard session wrote a ledger row anyway, with the shadow banner at the top of its prompt. The fix was not a better banner. The engine now reads a mode from the environment, and in shadow every command that records a publish or a send exits 3 and writes nothing, a passing gate exits 3, and the session cannot fill forms or attach files. A prompt is an instruction; an exit code is a lock. 27 invariants, 57 differential cases, 2,760 ledger rows reconciled row-for-row.

On 2026-09-22 a shadow session in my LinkedIn engine wrote a ledger row it had been told not to write. The instruction was the first thing in its prompt, in a banner, in capitals. It wrote the row anyway.

The conclusion first: a prompt is an instruction, and an instruction is not a lock. If an agent must not do a thing, the code the agent calls has to refuse, and the refusal has to be an exit code the runner can see. I had known that for irreversible actions. I had not applied it to a mode flag, because "shadow" felt like a setting rather than an action. It is an action. Every publish is one.

What the engine is

The engine is a gated, ledger-backed system for one operator's LinkedIn presence: three posts a day on a learning schedule, one considered comment per run, warm outreach inside hard budgets, and a job-search lane. A control plane on Cloudflare decides when. A small runner on my own machine drives my own logged-in browser, because LinkedIn offers no API for most of this. Every run is a fresh session with one playbook and a mode banner; the ledgers are the only memory.

Over three days, 2026-09-21 to 2026-09-22, the engine went from a folder of scripts to a package with 27 named invariants, each with a test that is the definition. Twenty-four ledger tables live in D1 now, generated from one schema spec; 2,760 rows were imported and reconciled row-for-row. Fifty-seven differential cases prove the lifted commands produce the same stdout, stderr, exit code and ledger writes as the originals, modulo an enumerated list of deltas. Fifteen schedules run in shadow. None of that stopped the row.

What happened

The scorecard job reconciles accepted invites in the browser and logs the week. Its first live shadow run was a success in every way I had planned to measure: the session started, the browser worked, the playbook ran, and the report came back. Then I read the ledger. There was a new row that a shadow run has no business writing.

The session had the banner. It had the playbook line that says shadow logs and never sends. It had the same model that had followed the same rule in fourteen other shadow runs. On this one it did not. I do not know why, and I have stopped treating "why" as the useful question. The useful question is what stopped it, and the answer was nothing. The commands the session called did not know they were in shadow. Only the prompt did.

The fix

The runner now sets an environment variable that names the mode, and the engine reads it. In shadow:

  • Every command that records a publish or a send exits 3 and writes nothing. That is log-post, log-comment, log-send without the shadow flag, log-reply, and the apply record.
  • A passing gate exits 3. The gate still runs, still scores, still reports. It cannot return the code that means "go".
  • Easy Apply preflight fails.
  • The session is started with the tools that fill forms and attach files disallowed, so even a session that decided to try has nothing to try with.

Exit 3 is deliberate. Zero means done, one means error, two means "correctly declined" in this engine's vocabulary. Three means held. The runner treats it as a normal outcome and the console shows it as one, so a shadow run that does exactly what it should now ends in a code that a person can read as "the lock worked".

The playbooks were rebuilt with the same banner, because the banner is still useful. It tells the model what is expected, and most of the time the model does it. The lock is for the rest of the time.

Why this keeps coming up

This is the third time on this site I have written the same finding in a different costume. The approval-gates essay said that anything the agent cannot undo has to be a property of the tool surface, not a policy asking the model to behave. The statistical-gating essay said a gate that cannot detect the effect it was built for is decorative. This one says a mode that lives only in the prompt is decorative too.

The pattern is the same each time. I write a rule in the place where I write, which is the prompt, and I feel the rule is enforced because I can see it. Then the system does the thing once, and I discover that the only enforcement was my having written it down. The rule has to move from the place where I write to the place where the code runs, and it has to fail closed.

The engine has a table of invariants for exactly this reason. Each one is marked Code, Partial or Gap. Code means the engine enforces it and a test proves it. Partial means the engine enforces its half and the rest is a playbook instruction. The mode flag was not even on the table, because I had not thought of it as an invariant. It is now: shadow never publishes, and the test is a session that tries.

Two more gaps closed the same week

Reading the ledger for the shadow row turned up two smaller ones, both the same shape.

A veto in the engagement candidate list could be overridden by an explicit lane or relevance argument on the command line. That is a rule living in the default rather than in the code. Now the pick filters on the stored lane and a veto is final.

The posting gate checked numbers against the fact ledger but let an unregistered number through if it was plausible. A checkable number found in neither the fact ledger nor a sourced claims row is now a hard fail, which is what the posting playbook had said all along. The playbook said it; the gate did not do it. Same costume.

What to copy

If you run agents on a schedule, list every action that must not happen in a given mode, and for each one ask which line of code refuses it. If the answer is "the prompt", you do not have a lock. Add a mode to the environment, make the command exit non-zero and write nothing, and then run a session that tries. The session that tries is the test. Mine wrote a row; yours will do something else. The lock is what makes it a story instead of an incident.

All writing · Projects · Michael Kaminski


Book a Call · Download Resume · MKaminski1337@gmail.com · (404) 838-8613 · LinkedIn · GitHub · DEV · X