Chris Lorenzo

homelab and infrastructure notes · stayz3ro

entry-47

A long session of parallel coding agents, and the one guardrail I broke

I used several Claude sessions and other CLI tools for a repo migration. Peer confirmation, the permission classifier, and a cold-read handoff test all held. The secrets rule did not, and the tokens now wait for rotation.

revisions rev. 1 first set 2026-10-23 last revised 2026-10-23 6 min read

Update (2026-10-19): The rotation follow-up records replacement, revocation and post-revocation checks; the pending rotation below was the state at the end of this session.

One long day in October, I used a small herd of coding agents for my homelab work. I used several Claude sessions in terminal panes and a couple of other CLI tools beside them, then split the work and collected results in one coordinating session. The job was a repo migration: moving my private repos to a self-hosted git server, wiring mirrors back to GitHub, setting branch protection, updating the state file I provide at the start of each fresh session.

I expected to spend the day watching for overlapping work across sessions. That happened once. More surprising was the guardrail record: most held with no effort from me, one gap in my coordination caused the only collision, and the rule that actually failed was one I broke myself.

I approve each session separately

The first symptom was pace, not danger. I kept hitting confirmation pauses in the peer sessions at live changes: creating a repo, pushing, enabling a mirror. It felt like friction; my first guess was that I had written the briefs wrong or left out context.

Narrowing it took one pass over what actually stopped. Reads ran freely in every session. Only live changes paused, in every peer session, regardless of brief. And every confirmation prompt came to me, never the coordinating session. That is not misconfiguration; it is the rule in the session instructions doing its job: approval must come from me.

The rule I keep: approval flows from me to each session directly. Approval in one session does not carry over to another.

I checked the two commands the classifier blocked

Mid-session, I hit two blocks from the permission classifier. Once on a history rewrite: rewriting the author identities on a long feature branch with a mailmap, then a force-push with lease. Once on a permission change on the public side: deleting branch-protection rulesets so a mirror could keep pushing.

My first guess was that the blocks were overly cautious and I should just approve the commands and move on.

Then I looked at what had triggered the blocks. Of everything I ran through a dozen parallel sessions that day, those two are where a mistake is unrecoverable or very visible: a history rewrite that lands wrong, a protection change that opens every public repo at once. It had ranked the day’s danger correctly.

So I did not click through. I ran both myself: the rewrite with lease protection after a dry run, and the ruleset deletions only after saving each old ruleset as JSON first.

The rule I keep: when the classifier trips, I take the wheel. A block means I need to review and run the command myself.

The state file failed twice before it passed

The migration ended with a state file adoption: the markdown file that records the current workstream, what is authorized, what is blocked, what is done. Before adopting it, I ran a handoff test in a second, fresh session: limit the test to the allowed files, get a statement of the situation, and require a stop if the file contradicts itself.

First run: FAIL. The file marked one workflow live-confirmed in its header and still listed it as untested in three later sections. A set of repo deletions was done in one paragraph and pending in another. The tail still called itself a draft awaiting review while the header said adopted. Second run after fixes: FAIL, narrowly; one paragraph and the tail still disagreed. Third run: PASS.

My wrong first guess was that the file was fine because I had written it carefully. Writing carefully is not the same as being consistent across six mentions of the same fact, and I would not have caught the drift in my own reread, because I knew what I meant.

The rule I keep: a state file is not done when I finish writing it. It is done when I can use a fresh session to read it and get a statement of the situation without a contradiction stopping the test. A handoff test that fails is the test working.

The gap: two sessions, one repo

The one collision of the day: a repo ended up with two push-mirror entries.

My first guesses were mechanical: a mirror bug, an API retry, a script firing twice. Narrowing it meant checking what I had actually run in each session, and the answer was dumber. I had put that repo on two parallel work lists. I had not made ownership exclusive, so I ran the migration twice. The script did exactly what it was told, twice.

The fix: delete the duplicate through the API, then add a verification step asserting exactly one push mirror per repo. The rule I keep: I assign work exclusively through the coordinating session, and I verify it assuming coordination will fail somewhere. Avoiding a collision by chance is luck. A check that would have caught the collision is a guardrail.

The honest one

The guardrail that failed was the secrets rule, and I am the one who broke it.

During the migration I pasted access tokens into the chat. A classic personal access token for the mirror bot, two fine-grained ones, and another generated in-session and embedded in the mirror clone URLs. Working through the CLI tools, I kept them in temp files and used them in scripts and API calls to finish the job.

My first cleanup guess was that deleting the temp files settled it. It does not. Everything that passed through the session sits in the transcript on disk. Deleting the files I can see does not touch the copy I cannot.

The fix has two parts. Rotation: every token that touched a session that day is on the rotation list, recorded in my backlog, not yet rotated, which is exactly where the honest version of this post leaves it. Rule: secrets now enter only through hidden prompts, typed where they do not echo into any transcript. If I need a credential for a CLI tool, I create it in the provider’s UI and paste it into a hidden prompt. Never into the chat, never onto a command line, never into a file generated in a session.

What held

The day worked because the rules are boring and mechanical. I approve work in each session separately. I review blocked commands myself. I test state files with a cold read before adoption and verify the result assuming something ran twice. The failures were both mine: sloppy task ownership between parallel sessions, and treating the chat as a reasonable place for a credential.

The herd is worth running again. Next time the tokens never enter the transcript at all, and the rotation list from this time gets closed out first.

Related posts: Forgejo as primary, GitHub as a mirror, Protect the branch where the pushes actually land.