oficina / docs Public

AGENTS.md: .git/config.lock is a bind mount, and a pipe hides the exit code open

On branch agents-config-lock

Two environmental traps, added to AGENTS.md's "tools that answer wrongly instead of failing". Doc-only, one commit, 1368c23, branched off main at 1b6d392.

Both cost time landing the triage branch tonight, and both are permanent properties of this environment rather than bugs in anything — which is exactly why they belong in AGENTS.md rather than in a branch discussion nobody will read again.

.git/config.lock is a read-only bind mount, not a stale lock

I hit it, wrote it down as debris someone should clear, and said so in my triage report. That was wrong. The paste-fidelity session went to remove it, got "Device or resource busy", and checked /proc/mounts:

/dev/nvme0n1p2 /home/paulca/amadan.net/oficina/docs/.git/config.lock ext4 ro,nosuid,nodev,relatime,...

Confirmed here independently before writing it up — this repo's own rule about not passing on another session's finding as your own verification, which I had already broken once on #11. It is the same harness mechanism .gitignore documents for .gitconfig and .bashrc: zero-byte read-only placeholders. Its evident purpose is to make git config unwritable while leaving the rest of .git writable. No session can clear it and none should try.

The part worth writing down is not "config is unwritable", it is the partial completion. git checkout -b <new> <base> updates the index and the working tree and then fails on the config write for upstream tracking. HEAD stays on the old branch while the tree holds the new base's content — so a branch you have been working on all night appears to have lost every change, as deletions. Nothing is actually gone if it is committed, and git reset --hard <sha> puts it back, but the first reading is that the tool ate your work. That is a bad thirty seconds and it will happen to every session that uses -b without --no-track.

So: always git checkout -b <new> --no-track <base>, never git push -u. --no-track is not a workaround, it is the permanent spelling. This branch was created that way and switched cleanly, which is at least self-consistent.

Also explained: the "unable to get credential storage lock … Read-only file system" both of us have been seeing on fetch and push. Same family, not worth chasing.

A pipeline reports its last command's exit code

Recorded by the word-processor session earlier tonight and now in the same list, because it kept arriving from new directions:

  • bash .amadan/ci.d/10-gate | tail -40 gives you tail's status — always 0 — while a real cargo fmt --check failure scrolls past inside output you have just read. I did this all night. My local gate runs were reading tail's exit code, and the only reason the readings were sound is that the gate prints its own gate: ok line and CI ran it independently.
  • amadan ci status … | head read as exit 0 while the command had actually failed with "not an amadan remote".
  • The pipefail variant bites the other way: strings "$bin" | grep -q "$sha" exits early on a match, SIGPIPEs strings, and pipefail calls the pipeline failed — so a release binary that was correctly version-stamped read as unstamped, minutes before shipping it.

Run the thing, capture its status, then read the log. Count into a variable rather than using grep -q.

Verification

Doc-only, no code paths touched. CGO_ENABLED=0 go build ./... && go test ./... green; CI running on the branch. Not merging until it is green and Paul has had the chance to object — it is his file and his rules section.

— claude~paul@keymail.dev, session "discussions triage overnight"

Third entry added at 72a0f20, on the paste-fidelity session's suggestion, and it is the right one to add: my two exit-code cases and their amadan ci status | head are all the same thing — a check reporting success about something it never examined — and the artifact version is the worse half. A wrong exit code you re-check in a second; a stale artifact you believe for hours.

The instance is this branch's own verification

Breaking fixtures/jump-to.mjs to prove a new check could fail came back 44/44 green. fixtures/node_modules was missing, node fixtures/build-bundles.mjs failed, and scripts/browser-drive.sh drove the committed bundle — which still had the working code in it. Nothing lied about a status. The drive tested something it had never looked at, and had no way to say so.

That is the most dangerous single moment of the night: had I accepted it, I would have recorded "confirmed red against a deliberate break" for a check that had never been exercised, and the whole verification story on #26 would have been fiction resting on one silent npm failure.

The gate's own cleanup, verified rather than relayed

They reported that a failed gate deletes the bundle you just built. Checked it in .amadan/ci.d/10-gate:110-121 before writing it down:

git checkout -- internal/docs/static/
git clean -fd -- internal/docs/static/ >/dev/null

It is deliberate, not a bug, and the step's own comment says why: without the restore, a second run would pass on a rebuild nobody committed. The consequence for whoever hits it is that git add is not enough — staged-but-uncommitted still reads as drift — so rebuild and commit together.

The general form

Theirs, and now the entry's closing line: before trusting a green run, make it fail on purpose. It is the only reason the stale bundle was caught, and it is the mechanism behind the rule already two paragraphs below it in AGENTS.md ("a new test must be seen to fail"). The addition is that seeing it fail is not enough on its own — you also have to know the artifact under test was actually rebuilt, or the break you staged never reached the thing you drove.

State

Two commits, doc-only. CI passed at 1368c23; running on 72a0f20. Still not merging until that is green and Paul has had the chance to object — it is his rules section, and nothing here is urgent.

— claude~paul@keymail.dev, session "discussions triage overnight"

Whole-branch review by Fable, and what it changed — 1ebea01

Done per AGENTS.md's highest-capability rule. Two blocking findings, and the first is the reason a doc-only branch still gets reviewed.

BLOCKING — the recovery advice would have destroyed uncommitted work

I wrote that a failed checkout -b is recovered with git reset --hard <sha>. The failure carries any uncommitted edits into the new tree, so reset --hard there destroys them. That is advice that loses work, in an entry whose entire purpose is to stop someone panicking at a tree that looks emptied — the worst possible place for it.

The right recovery is git checkout <new>. The failed command already created the branch ref, and the tree already matches it, so this finishes the switch minus the tracking you cannot have. Reproduced here before rewriting it, with an untracked file present:

checkout -b recovery-probe origin/main  -> exit 1
  HEAD:              still agents-config-lock
  recovery-probe:    created, at 1b6d392
  SCRATCH-TEST.txt:  still there
git checkout recovery-probe             -> exit 0, "Switched to branch"
  SCRATCH-TEST.txt:  still there

reset --hard is now labelled as the option for when you have nothing uncommitted. The entry also now says the ref exists, which matters because retrying -b with the same name is refused and that is confusing on top of an already confusing failure.

BLOCKING — config.lock was not the mechanism

The bullet's opening line hung on it being the cause. It is not. /proc/mounts carries four read-only bind mounts under .git:

.git/hooks             ro
.git/config            ro
.git/config.lock       ro
.git/config.worktree   ro

config itself being read-only is why config is unwritable; config.lock is merely the first barrier git hits, which is why the error reads "File exists". Confirmed with awk over /proc/mounts here rather than taken from the review.

So this is now the second correction to the same three sentences: first I called it stale debris, and the paste-fidelity session corrected that; then I got the causal mechanism wrong, and the review corrected that. Both were confident and both were wrong. The observable facts I quoted were right each time, which is precisely why neither error announced itself.

Three overstatements, corrected

  • Not "every git operation that writes config". branch.autoSetupMerge is unset here, so checkout -b <new> <local-branch> succeeds. The trap needs a remote-tracking base. That is the base this file prescribes, so the advice stands unchanged, but the stated scope was wrong.
  • .gitignore's placeholders are character devices, not bind mounts, and the credential-store "Read-only file system" is the sandbox's write allowlist rather than a mount at all. I had called both "the same mechanism". They are cousins.
  • browser-drive.sh drives what is on disk, not what is committed — //go:embed takes it at build time, so an uncommitted rebuild would be tested. And a failed gate discards an untracked rebuild; a staged one survives git clean and merely still fails the drift check. "Deletes the bundle you just built" was half right.

Also

The pipe entry now says | tail hides a status only when pipefail is unset, and carries the measurement behind the SIGPIPE claim: a sha near the start of a 20MB binary returned 141 on ten runs out of ten, the same sha at the very end returned 0 every time. So the check's answer depends on byte offset — which is a sharper point than "it can fail", and it was missing.

What the review checked and found correct

The checkout -b failure itself, reproduced twice: exit 1, HEAD unmoved, index and tree rewritten to the base. --no-track clean at exit 0. git config --local exit 255 with the same error. The pipefail/SIGPIPE behaviour, measured rather than reasoned. 10-gate:108-123 doing exactly what the entry says, and the entry's account of why matching the gate's own comment. No duplication with the existing carlos channels, git branch --merged, or "a new test must be seen to fail" entries.

State

Three commits, doc-only, pushed. CI passed at 1368c23 and 72a0f20; running on 1ebea01. Still not merging — it is your rules section, and after two wrong versions of the same paragraph I would rather you saw the third than that I landed it at 2am.

— claude~paul@keymail.dev, session "discussions triage overnight"