workflow-fix: guard repo-root working-tree reverts (restore/checkout ./clean -f)
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Overview / Motivation
Auto-filed by the workflow-fix-on-bug protocol from a prose follow-up surfaced on task #841 (emitting agent: analyzer, clean-result revise round).
Goal
Extend the repo-root destructive-git guard (PreToolUse hook + lint) to cover working-tree reverts — git restore . / git restore <path>, pathspec git checkout . / git checkout -- <path>, git clean -f, and unqualified git reset --hard — not only branch switches (hook) and doc-mentioned reset --hard (lint).
Workflow gap
- Bug observed: during #841's clean-result revise round (2026-07-02 ~22:42Z), a concurrent session's destructive working-tree git op on the shared repo root reverted the analyzer's uncommitted
body.mdedits mid-task — the file snapped back to HEAD. The analyzer recovered by re-applying viatask.py set-body(atomic commit). Same hazard class as the #778 repo-rootreset --hardthat clobbered #812/#813 task state. - Why it is a workflow gap: CLAUDE.md bans destructive
git reset --hard/git clean -f/git checkout ./git restore .on the repo root and namesguard_repo_root_branch.shas enforcement, but the hook only gates BRANCH ops (grep -qE '\bgit\b.*\b(checkout|switch)\b'at scripts/guard_repo_root_branch.sh:102, blocking-b/-B/--detach/<branch>forms) — a pathspecgit checkout .,git restore .,git clean -f, and runtimegit reset --hardall pass the hook. The lint (workflow_lint.py --check-no-repo-root-git-reset-hard,_GIT_RESET_HARD_RE) covers onlyreset --hardand only in agent/skill DOC text, not runtime. So the prose rule has no runtime teeth for the working-tree-revert class that just caused a live edit loss. - Confidence (emitter): high (live incident on #841; hook + lint coverage verified by grep).
Proposed change (candidate diff sketch — refine in planning)
scripts/guard_repo_root_branch.sh: add a second guard clause for destructive working-tree ops when the effective cwd is the repo root (notgit -C <worktree>-qualified):- block
git restorewith a pathspec unless--staged-only-with-explicit-path… (planner decides exact policy; minimum: block baregit restore ./git checkout ./git checkout -- <path>/git clean -f*/git reset --hardat repo root) - preserve the existing
-C-qualifier waiver and an explicit same-line allow-sentinel for deliberate uses.
- block
scripts/workflow_lint.py: extend_GIT_RESET_HARD_REcoverage (or a sibling check) so workflow docs prescribinggit restore ./git checkout ./git clean -fon the shared root FAIL the same wayreset --harddoes.- Tests: hook-behavior cases (blocked forms,
-Cwaiver, sentinel) + lint regression cases.
Scope / surfaces
- Primary target:
scripts/guard_repo_root_branch.sh, scripts/workflow_lint.py - Grep the workflow surface for prescriptive uses before editing (
grep -rnE 'git (restore|checkout) \.|git clean -f' .claude/ CLAUDE.md scripts/) and whitelist/fix every legitimate hit; list them in the plan.
Constraints / invariants
- Workflow-surface only. Existing hook tests +
workflow_lintdefault run stay green; per-worktreegit -C "$WT"destructive ops remain allowed. - Fail-closed parsing consistent with the hook's #804 latch semantics.
- This session runs under
EPM_WORKFLOW_FIX_SESSION=1and carries aworkflow_fix_target:Provenance line — recursion guard applies.
Provenance
- workflow_fix_target: scripts/guard_repo_root_branch.sh, scripts/workflow_lint.py
- fingerprint: b9c2b2c3249d
Surfaced prose (verbatim, from the #841 analyzer's revise-round report): "my initial in-place Edits to body.md were reverted mid-task by a concurrent destructive git op on the shared repo root — body.md snapped back to HEAD (git-clean, 3 pre_reg, 4 images) around 22:42Z. This is the #778/#812/#813-class hazard. I recovered by applying via set-body (atomic commit, durable). The existing lint (workflow_lint --check-no-repo-root-git-reset-hard) covers reset --hard, but a git restore ./git checkout . working-tree revert may not be guarded — worth a look if you see other sessions losing uncommitted edits."