ADR 0029 omission is bypassed by a hook-silent git checkout -- <path> #554

Open
opened 2026-09-21 02:08:08 +00:00 by hexajon · 0 comments
Owner

The hole

ADR 0029 omits worktree remove --force from every surface, so madtea cannot discard a dirty worktree. Its Context section rests that on two layers: the omitted flag, plus the plugin hooks that steer callers off raw git. It states the two layers are independent, neither depending on the other behaving.

Both layers missed the shortest path to the same loss:

git checkout -- internal/service/git/status.go internal/format/git/status.go ...
madt_worktrees(action="remove", path=".worktrees/x")

Step one destroys every uncommitted change in the named files. check-git-local.sh classifies git checkout -- <path> file restore as fully SILENT: no deny, not even an advisory. Step two then succeeds, because the omission's protection is a dirty-tree refusal, and the tree is no longer dirty. The work is gone, and no layer ever fired.

I did exactly this today, to four files, in one command. Nothing warned me.

Why the omission does not cover it

The per-operation table justifies dropping the force flag with "a clean worktree already removes without force, so dropping the flag loses nothing recoverable-affecting". That holds only while MAKING a worktree clean is itself guarded. It is not. Cleaning is one silent command away, so remove without force= is remove --force in two steps.

Clause 3 also assumes the escape is mine to run: "the refusal names the tool the user already holds, raw git in their own shell". Here no refusal was involved and no terminal of mine was used. The destruction ran through the plugin's own hooked Bash path, in a form the plugin deliberately stays silent on.

The inverted priority

The hook suite guards the RECOVERABLE destructive forms and ignores the unrecoverable one:

  • git branch -D destroys a ref only. The commits survive in the reflog. madt_branch_delete omits force, and ADR 0029 treats this as a headline protection.
  • git checkout -- <path> and git restore <path> destroy uncommitted content. There is no reflog for a working-tree file. Both are silent.

The second is the only one of the two that loses work which never existed anywhere else.

What needs deciding

This is a question about ADR 0029's scope, not only a matcher gap, so I do not want it closed by quietly adding a pattern.

  1. Does the omission rule extend to raw forms that RE-ENABLE an omitted operation, or does it govern madtea's own surfaces only? If surfaces only, the two-layer claim in the Context section needs correcting, because the second layer does not exist for this case.
  2. If it does extend, then git checkout -- <path>, git restore <path>, and git stash drop / git stash clear each need a classification. ADR 0021 makes these a hard case: they are not reads, so its advisory rule does not reach them, and a deny has no madt_* replacement to point at, because madtea deliberately offers no discard.
  3. A refusal here cannot follow clause 3 as written. Naming raw git as the escape names the exact command being refused. The only escape that is not the thing itself is a terminal outside the hooked path.

My lean is that the guard belongs on the act of making a tree clean rather than on the removal that follows, and that it must not be satisfiable by running the destructive form first.

Acceptance criteria

  • ADR 0029 either records that it governs madtea surfaces only, with the two-layer claim corrected, or is amended to cover raw re-enablement of an omitted operation.
  • git checkout -- <path>, git restore <path>, and the stash discard forms each carry a recorded classification with its reasoning, whichever way it goes.
  • Whatever lands is covered by hooks/tests/check-git-local.test.sh.
  • The reasoning is recorded before any matcher change, so the fix does not land ahead of the decision.
## The hole ADR 0029 omits `worktree remove --force` from every surface, so madtea cannot discard a dirty worktree. Its Context section rests that on two layers: the omitted flag, plus the plugin hooks that steer callers off raw git. It states the two layers are independent, neither depending on the other behaving. Both layers missed the shortest path to the same loss: ``` git checkout -- internal/service/git/status.go internal/format/git/status.go ... madt_worktrees(action="remove", path=".worktrees/x") ``` Step one destroys every uncommitted change in the named files. `check-git-local.sh` classifies `git checkout -- <path>` file restore as fully SILENT: no deny, not even an advisory. Step two then succeeds, because the omission's protection is a dirty-tree refusal, and the tree is no longer dirty. The work is gone, and no layer ever fired. I did exactly this today, to four files, in one command. Nothing warned me. ## Why the omission does not cover it The per-operation table justifies dropping the force flag with "a clean worktree already removes without force, so dropping the flag loses nothing recoverable-affecting". That holds only while MAKING a worktree clean is itself guarded. It is not. Cleaning is one silent command away, so `remove` without `force=` is `remove --force` in two steps. Clause 3 also assumes the escape is mine to run: "the refusal names the tool the user already holds, raw git in their own shell". Here no refusal was involved and no terminal of mine was used. The destruction ran through the plugin's own hooked Bash path, in a form the plugin deliberately stays silent on. ## The inverted priority The hook suite guards the RECOVERABLE destructive forms and ignores the unrecoverable one: - `git branch -D` destroys a ref only. The commits survive in the reflog. `madt_branch_delete` omits force, and ADR 0029 treats this as a headline protection. - `git checkout -- <path>` and `git restore <path>` destroy uncommitted content. There is no reflog for a working-tree file. Both are silent. The second is the only one of the two that loses work which never existed anywhere else. ## What needs deciding This is a question about ADR 0029's scope, not only a matcher gap, so I do not want it closed by quietly adding a pattern. 1. Does the omission rule extend to raw forms that RE-ENABLE an omitted operation, or does it govern madtea's own surfaces only? If surfaces only, the two-layer claim in the Context section needs correcting, because the second layer does not exist for this case. 2. If it does extend, then `git checkout -- <path>`, `git restore <path>`, and `git stash drop` / `git stash clear` each need a classification. ADR 0021 makes these a hard case: they are not reads, so its advisory rule does not reach them, and a deny has no `madt_*` replacement to point at, because madtea deliberately offers no discard. 3. A refusal here cannot follow clause 3 as written. Naming raw git as the escape names the exact command being refused. The only escape that is not the thing itself is a terminal outside the hooked path. My lean is that the guard belongs on the act of making a tree clean rather than on the removal that follows, and that it must not be satisfiable by running the destructive form first. ## Acceptance criteria - ADR 0029 either records that it governs madtea surfaces only, with the two-layer claim corrected, or is amended to cover raw re-enablement of an omitted operation. - `git checkout -- <path>`, `git restore <path>`, and the stash discard forms each carry a recorded classification with its reasoning, whichever way it goes. - Whatever lands is covered by `hooks/tests/check-git-local.test.sh`. - The reasoning is recorded before any matcher change, so the fix does not land ahead of the decision.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#554
No description provided.