CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

autorepair wrote an empty commit directly to a repo's default branch#218

Openccantynz opened this issue 5d ago
ccantynzAuthorcommented 5d ago

Two defects in one commit, found while investigating why ccantynz/Vapron looked stale.

2528cd45  gluecron[bot] <bot@gluecron.com>  2026-08-11  "fix: auto-repair by gluecron"

It is the newest commit on that repo's default branch (Main), and git show --numstat returns nothing — it changed no files. Independently confirmed by the Vapron-side session, which found the same commit in a stale remote-tracking ref and verified it is empty.

1. It produced no diff

Autorepair decided a repair was warranted and then wrote a commit containing no changes. Whatever signal triggers it can fire on nothing. Either the detection is wrong, or the repair produced an empty patch and the commit was made anyway without checking.

An empty commit is the mild version of this failure. The same "act without verifying there is anything to do" path is what would produce a wrong commit.

2. It wrote to a default branch unreviewed

A bot committed straight to Main. Not a branch, not a PR — the default branch of a repository, with no review. That is the shape flagged previously as "autorepair fails OPEN"; this is a concrete instance of it landing.

Note the interaction with branch protection: whatever rules exist on that repo did not stop it. Worth confirming whether the bot path bypasses matchProtection entirely or whether the repo simply had no rule.

Why it mattered here

This commit is the reason the repo appeared to have recent activity. Its default branch showed an August commit while no human had touched it since 20 July. The bot manufactured a signal of liveness on a repo that was in fact frozen — the same "advertises liveness, proves deadness" pattern as the mirror gap in the import path (fixed separately).

Anyone triaging that repo, including me, initially read "last commit 11 Aug" as evidence it was maintained.

Suggested fixes

  1. Never commit an empty tree change. If the repair produces no diff, log it and stop. Cheap, and it removes the whole class.
  2. Route repairs through a branch + PR, or at minimum respect branch protection on the default branch. A bot with unreviewed write access to main is a supply-chain concern regardless of intent.
  3. Make the audit trail say why. "fix: auto-repair by gluecron" with no diff and no reason is unauditable after the fact.

Not fixed here

Filing rather than fixing because (2) changes the autorepair authorisation model and could stop repairs that are currently relied on. That is an owner decision, not a drive-by change.

ccantynzcommented 5d ago

Root cause found — the "off" switch does not govern the write path

Lead came from the Vapron-side session checking their own exposure (they have 0 gluecron[bot] commits ever, so the capability exists in the shared build but has not fired there). Verified here against this codebase.

There are two settings systems, with opposite defaults, and autorepair reads the permissive one.

src/lib/automation-settings.ts:68     autoRepairMode: "off"        ← default OFF
   table: repo_automation_settings (migration 0106)

src/lib/gate.ts:566
   const enableRepair = opts.enableAutoRepair !== false
                     && settings?.autoFixEnabled !== false;        ← reads a DIFFERENT table
   table: repo_settings
   schema.ts:302  autoFixEnabled: boolean("auto_fix_enabled").default(true).notNull()

getOrCreateSettings (repo-bootstrap.ts:266) inserts a row on first read for any repo that lacks one, taking the schema default. So every repository on the platform ends up with auto_fix_enabled = true, and enableRepair is true for all of them.

Meanwhile autoRepairMode — the setting the Automation page presents, documented as defaulting to 'off'is never consulted at the gate. It is decorative with respect to this write path.

So: a bot with unreviewed write access to default branches is on by default for every repo, and the switch a user would reach for to turn it off does not control it.

That is the answer to "why did it fire on a repo nobody enabled it for." ccantynz/Vapron was an imported-and-never-configured repo — precisely the no-settings-row state that gets the permissive default.

The invariant this breaks

automation-settings.ts's own header states the contract:

Dispatch sites keep their existing env guards; this module only ever narrows what runs, never widens it.

!== false against an absent row widens by default. The invariant was intended and is not held.

Note the same shape governs four more gates on the lines directly above:

settings?.gateTestEnabled  !== false
settings?.secretScanEnabled !== false
settings?.securityScanEnabled !== false
settings?.aiReviewEnabled  !== false

Those default-on are mostly benign — running a scan nobody asked for is not the same as committing to their default branch. Worth a look regardless, since the polarity reasoning is identical.

Why I have not just fixed it

Making the gate honour autoRepairMode is one line, and it is the correct reading of the documented contract — but it flips effective behaviour from on to off for every repo on the platform in a single deploy. If anything currently depends on autorepair running, that stops silently.

That is an owner decision on automation authorisation, not a drive-by change. Options, in order of my preference:

  1. Gate reads autoRepairMode, and repairs go to a branch + PR instead of the default branch. Closes both halves: the off-switch works, and the remaining writes are reviewable. Behaviour change is visible rather than silent.
  2. Gate reads autoRepairMode only. Honours the documented default; disables autorepair platform-wide until re-enabled per repo.
  3. Align the two defaults and keep it on, but block writes to a default branch and never commit an empty diff. Least disruption, still removes the sharp edge.

All three should include the empty-diff guard from the original report — that half is an unambiguous bug with no policy question attached, and it is the cheapest thing here.

Two sources of truth, again

This is the same pattern behind most of what this audit trail keeps finding: two places that both claim to describe one fact, disagreeing, with the consumer reading whichever was written first. The durable fix is not choosing a winner — it is that repo_settings.auto_fix_enabled and repo_automation_settings.auto_repair_mode should not both exist.

c comment · e edit title · x close/reopen · ? shortcuts