Passante A Passante publication

En Passante

In Passing
Field Kit

The hook warned, 42 branches went unmerged

A debt limit that only prints advice does not hold, so the check must return non-zero and refuse the write.

What happened

On 2026-09-08, a repository's hooks printed warnings about branch debt that the automated agent working in it simply skipped past, and 42 unmerged branches accumulated before anyone converted the warning into a hard stop, whose override then failed and blocked the cleanup work itself.

2026-09-08 Git hooks · an agent-driven local repository measured: 42 unmerged branches accumulated under warning-only hooks
3 prompts, in order
  1. Count the debt your warning allowed Hooks print debt warnings and the debt keeps growing anyway.
  2. Turn the warning into a refusal The audit shows every debt check exits zero.
  3. Prove the gate blocks and clears The blocking hook is installed and cleanup is still pending.

1Count the debt your warning allowed

Run it when
Hooks print debt warnings and the debt keeps growing anyway.
It asks you for
Every hook that mentions branches or debt, plus its exit codes and the current branch listing.
You get back
A debt count and a per-hook verdict of blocking or advisory, with the exit paths named.
It refuses if
The hook source is unavailable or the branch listing output is not supplied.
copy this into a model
<stance>

The hooks in this repository claim to protect branch hygiene. Printed text protects nothing. An advisory line scrolls past an agent and past a person, and the unmerged count climbs quietly. A warning-only setup in one project let 42 unmerged branches accumulate before anyone looked. Your task in this run is measurement only. Read what each hook does on exit and report the current debt. No repair work here.

</stance>

<inputs>

Ask for all four items. Wait for each answer. Do not proceed on a missing one.
1. The full contents of every hook that mentions branches, debt or cleanliness.
2. The exit code each of those hooks returns when it detects debt.
3. The command the team uses to list unmerged branches, with its current output pasted in.
4. The write operations the team wants gated, such as commits or file edits.

</inputs>

<method>

1. Read each hook line by line. Trace every path that reaches an exit. Record the code returned on each path.
2. State for each hook whether any debt path returns non-zero. If none does, the hook is advisory.
3. Count the unmerged branches from the supplied output. Report the number as given.
4. Check whether the gated write operations actually pass through the hooks you read. Name any operation with no hook in front of it.
5. If the exit code on a path cannot be determined from the source, mark that path unknown. Do not guess from the message text.
6. If the branch listing was not supplied, report the count as unknown and stop there.

</method>

<deliver>

- Current debt count, with the command that produced it
- Per-hook verdict: blocking, advisory or unknown
- Exit paths found, with the line each one sits on
- Write operations that reach no hook at all
- What remains unknown after this pass

</deliver>

<refuse>

- Do not infer a hook's behaviour from its filename or its warning text.
- Do not propose or write a fix in this run.
- Stop if the hook source is missing. Name the exact file you need.
- Do not report a branch count from memory. Use only the pasted output.
- If no supplied hook covers the gated operation, say the repository has no gate.
- Do not describe an advisory hook as partial protection. Call it advisory.

</refuse>

2Turn the warning into a refusal

Run it when
The audit shows every debt check exits zero.
It asks you for
The debt threshold, the write operations to gate, the clearing action and the override channel.
You get back
A hook that exits non-zero with the count, and an override contract with one documented input.
It refuses if
No threshold is named, or cleanup work has no route through the gate.
copy this into a model
<stance>

A debt gate earns its name by returning non-zero. Everything else is decoration. The gate designed here can fail in opposite directions. It can let debt through, or it can block the very cleanup that would clear the debt. One project hit the second of those when a new hard stop shipped with a broken override, and legitimate cleanup stalled. Design against both at once.

</stance>

<inputs>

Ask for all five items. Wait for each. Do not fill a gap with a default.
1. The debt predicate in plain words, such as zero unmerged branches.
2. The threshold number, if the team wants anything other than zero.
3. The exact write operations to gate, and the hook that intercepts each one.
4. The action that clears the debt, phrased as something a caller can run.
5. The channel an override arrives on, and who is allowed to send it.

</inputs>

<method>

1. Write the predicate as a single command that returns a count. Show the command.
2. Place the check at the chokepoint every gated write passes. Name that chokepoint.
3. Make the debt path exit non-zero. Print the current count and the clearing action in the same message.
4. Define the override as one explicit input that a caller must supply deliberately. Write down its exact form.
5. Confirm the override cannot be set by the agent being gated through its own channel. If it can, the gate is bypassable. Say so.
6. Confirm the clearing action itself passes the gate. A merge or a branch deletion must never be refused.
7. If no override channel can be separated from the gated writer, stop. Report the limitation instead of shipping an unreachable override.

</method>

<deliver>

- The predicate command and its threshold
- The hook change, as a diff
- The refusal message, quoted exactly as callers will see it
- The override contract: input form, who sends it, where it gets recorded
- Proof that the clearing action is exempt
- Known gaps in coverage

</deliver>

<refuse>

- Do not keep any advisory branch that lets debt writes through.
- Do not ship an override the team cannot invoke by hand.
- Do not make the override a value that defaults to on.
- Do not raise the threshold to make the current debt pass.
- Stop if the clearing action is unknown. Ask for it and wait.
- Do not claim the gate works. Testing happens in the next step.

</refuse>

3Prove the gate blocks and clears

Run it when
The blocking hook is installed and cleanup is still pending.
It asks you for
A scratch branch, a covered write command, the override input verbatim and an undo path.
You get back
A four-case table of exit codes, a bypass finding and a single verdict.
It refuses if
No case produced an actual refusal, or the installed hook was never exercised.
copy this into a model
<stance>

The gate is a claim until a write gets refused in front of you. Reading the hook proves nothing about the hook that is installed. The outcomes that matter pull in opposite directions. A gate that never refuses leaves debt to pile up, and a gate whose override fails locks out the cleanup. Test both. Report exit codes, not impressions.

</stance>

<inputs>

Ask for all four items. Wait for each before running anything.
1. A scratch branch name that is safe to create and delete.
2. The exact write command the gate is supposed to cover.
3. The override input written out as a caller would supply it.
4. The undo path, so the repository returns to its starting state.

</inputs>

<method>

1. Create debt above the threshold using the scratch branch. Confirm the predicate command reports it.
2. Run the covered write. Record the exit code and the full message. Expect a refusal.
3. Clear the debt by merging or deleting the scratch branch. Run the same write. Expect success.
4. Re-create the debt. Supply the override exactly as given. Expect the write to pass and the use to be recorded.
5. Supply a malformed override. Expect a refusal, not a silent pass.
6. Attempt to set the override from the channel the gated agent controls. If that works, report the gate as bypassable.
7. If two cases return the same outcome, the test separates nothing. Name which case is still untested.
8. Restore the repository using the undo path. Confirm the branch count matches the starting number.

</method>

<deliver>

- A table with four rows: debt present, debt cleared, valid override, malformed override
- Exit code and message for each row
- The bypass finding from step 6, stated yes or no
- Verdict: gate holds, gate is bypassable, or gate blocks cleanup
- Repository state after restore, compared to the start

</deliver>

<refuse>

- Do not read the hook source and infer an outcome. Run the write.
- Do not report a pass without a captured non-zero exit code.
- Stop if no case produced a refusal. The gate is untested.
- Do not test a copy of the hook. Test the installed one.
- Do not call the override working until a blocked write passes with it.
- If the undo path leaves extra branches behind, say so before giving a verdict.

</refuse>
How you know it worked

Leave exactly one branch unmerged. Run a write operation the gate covers. The hook exits non-zero and prints the current unmerged count. Merge or delete that branch. Run the same write again and watch it succeed.

from a real incident, 2026-09-08 · stack Git hooks · an agent-driven local repository · verified by 42 unmerged branches accumulated under warning-only hooks · guardrailsgit-hookstechnical-debtagents

← every Field Kit